Sunday 12 January 2014

On plugin architecture

I was looking a bit a decoupling my code in a better way, for the moment Dx11 nodes follow the current way:

  • Implement IPluginEvaluate
  • Implement either ResourceProvider or ResourceContextProvider
Then when you Implement ResourceProvider you use : SetDevice(RenderDevice device)

In some way this forces you to register a SetDevice method (so you don't forget to grab device), but this is not ideal in many points.

One thing I don't like with this, you have too much cooking and so much work is wasted on initialization order. Let's take a very basic example:

Code Snippet
  1. [PluginInfo(Name = "CopyCounter", Category = "DX11.Buffer", Version = "", Author = "vux")]
  2. public class CopyCounterNode : IPluginEvaluate,IDxDeviceListener, IDxResourceContextProvider
  3. {
  4.     [Input("Buffer In", DefaultValue = 1,IsSingle=true)]
  5.     protected Pin<DX11Resource<DX11StructuredBuffer>> FInBuffer;
  6.  
  7.     [Output("Buffer Out",IsSingle=true)]
  8.     protected ISpread<DX11Resource<DX11RawBuffer>> FOutBuffer;
  9.  
  10.     private RenderDevice device;
  11.  
  12.     public void SetDevice(RenderDevice device)
  13.     {
  14.         this.device = device;
  15.         this.FOutBuffer[0] = new DX11Resource<DX11RawBuffer>();
  16.     }
  17. }

Please note that I omitted the update/evaluate for clarity.

So what happens is First object constructor is called (here a default constrcutor since none is specified),
then Buffer In and Buffer Out are injected into the node, to finish, once node is ready, SetDevice is called to the node.

Now if I add a constructor, we here have the same:

Code Snippet
  1. [PluginInfo(Name = "CopyCounter", Category = "DX11.Buffer", Version = "", Author = "vux")]
  2. public class CopyCounterNode : IPluginEvaluate,IDxDeviceListener, IDxResourceContextProvider
  3. {
  4.     [Input("Buffer In", DefaultValue = 1,IsSingle=true)]
  5.     protected Pin<DX11Resource<DX11StructuredBuffer>> FInBuffer;
  6.  
  7.     [Output("Buffer Out",IsSingle=true)]
  8.     protected ISpread<DX11Resource<DX11RawBuffer>> FOutBuffer;
  9.  
  10.     private RenderDevice device;
  11.     private IPluginHost host;
  12.  
  13.     [ImportingConstructor()]
  14.     public CopyCounterNode(IPluginHost host)
  15.     {
  16.         this.host = host;
  17.     }
  18.  
  19.     public void SetDevice(RenderDevice device)
  20.     {
  21.         this.device = device;
  22.         this.FOutBuffer[0] = new DX11Resource<DX11RawBuffer>();
  23.     }
  24. }

Please note that first here there's 2 things which are pretty bad:

  • I need to use ImportingConstructor otherwise MEF ignores injection (and plugin creation will fail). This is just really bad. That means I need to add an attribute on every single class of my model. Which, by propagating, would mean I'd need to use MEF even in my standalone library! Even worse, you also need to use attribute in every subclass, since they are not propagated.
  • Using attributes for input/output sounds a good idea, saves some time, but that's reasonably true for simple nodes, where you use inheritance, that creates issue. If I mark my pins as private, I get compiler warning (rightly so), you can use pragma to disable, but then you need to do that on all nodes, really intrusive as well.
Now one cool thing, I can export my DX11 Services, and have them injected, so instead I can do :

Code Snippet
  1. [PluginInfo(Name = "CopyCounter", Category = "DX11.Buffer", Version = "", Author = "vux")]
  2. public class CopyCounterNode : IPluginEvaluate, IDxResourceContextProvider
  3. {
  4.     [Input("Buffer In", DefaultValue = 1,IsSingle=true)]
  5.     protected Pin<DX11Resource<DX11StructuredBuffer>> FInBuffer;
  6.  
  7.     [Output("Buffer Out",IsSingle=true)]
  8.     protected ISpread<DX11Resource<DX11RawBuffer>> FOutBuffer;
  9.  
  10.     private RenderDevice device;
  11.     private IPluginHost host;
  12.  
  13.     [ImportingConstructor()]
  14.     public CopyCounterNode(RenderDevice device, IPluginHost host)
  15.     {
  16.         this.host = host;
  17.         this.device = device;
  18.         this.FOutBuffer[0] = new DX11Resource<DX11RawBuffer>();
  19.     }
  20. }

But this will of course miserably fail.
The simple reason is that FOutBuffer is injected after the constructor. You can't get a proper working object.

2 Solutions for this:

  • Implement IPartImportsSatisfiedNotification , and move the buffer initialization into this. Hurray,one more interface, one more remote method to implement, more intrusion.
  • Create pins by hand in constructor, via IIOFactory. Example below:

Code Snippet
  1.   [PluginInfo(Name = "CopyCounter", Category = "DX11.Buffer", Version = "", Author = "vux")]
  2.   public class CopyCounterNode : IPluginEvaluate, IDxResourceContextProvider, IPartImportsSatisfiedNotification
  3.   {
  4.       private Pin<DX11Resource<DX11StructuredBuffer>> FInBuffer;
  5.       private ISpread<DX11Resource<DX11RawBuffer>> FOutBuffer;
  6.  
  7.       private RenderDevice device;
  8.       [ImportingConstructor()]
  9.       public CopyCounterNode(RenderDevice device, IIOFactory iofactory)
  10.       {
  11.           this.device = device;
  12.           this.FInBuffer = iofactory.CreateResourceInputPin<DX11StructuredBuffer>("Buffer In",true);
  13.           this.FOutBuffer = iofactory.CreateResourceOutputPin<DX11RawBuffer>("Buffer Out",true);
  14.           this.FOutBuffer[0] = new DX11Resource<DX11RawBuffer>();
  15.       }
  16.   }

This feels a little bit more verbose, but now you have something correct (eg : when your object is instanciated it is ready to use, no more dodgy ordering. Initialization is in one place, and you can easily add some extensions methods/static methods to IOFactory to reduce overhead.

Another great advantage, you can create some nicer extensions like :

Code Snippet
  1. public static Pin<DX11Resource<DX11Layer>> CreateDX11LayerOut(
  2.     this IIOFactory iofactory,
  3.     RenderDevice device,
  4.     RenderDelegate<IPluginIO, RenderSettings> rendermethod,
  5.     string name = "Layer Out",
  6.     bool subscribe = true)
  7. {
  8.     var attr = new OutputAttribute(name) { IsSingle = true };
  9.     var pin = iofactory.CreateResourceOutputPin<DX11Layer>(name, subscribe);
  10.     pin[0] = new DX11Resource<DX11Layer>();
  11.     pin[0][device] = new DX11Layer();
  12.     pin[0][device].Render = rendermethod;
  13.     return pin;
  14. }

Now if you call CreateDX11LayerOut, you even took care of the naming by providing a default, which is much easier to mess up with attributes (random typo, no idea of naming...)

Last issue then, this ImportingConstructor is annoying, specially it needs to be everywhere.

So I tried to check If I could get rid of MEF (partly), and build a small plugin interface with a far better container, eg : Autofac

One thing I like is all initialization code is centralized (and not scattered randomly like in MEF so you don't know what exported/imported anymore).

Building container is rather simple:

Code Snippet
  1. ContainerBuilder cb = new ContainerBuilder();
  2. cb.RegisterInstance<IHDEHost>(hdehost).ExternallyOwned();
  3. cb.RegisterInstance<INodeInfoFactory>(ni).ExternallyOwned();
  4. cb.RegisterInstance<IORegistry>(ioreg).ExternallyOwned().As<IIORegistry>();
  5. cb.RegisterInstance<ILogger>(logger).ExternallyOwned();
  6. cb.RegisterInstance(this.RenderDevice).As<DxDevice,RenderDevice>().ExternallyOwned();
  7.  
  8. this.container = cb.Build();

Please note that I didn't added all services here, but you also have a lot of nitfy features, and using AutoFac I can now technically even use composition on my core runtime, which was not possible with MEF (except adding attributes everywhere).

More fun, I can import all vvvv services in one go using their Integration library.

Now to register a plugin:

Code Snippet
  1. container = parent.BeginLifetimeScope
  2. (
  3.     (cb) =>
  4.     {
  5.         cb.Register(c => pluginHost).As<IPluginHost, IPluginHost2, INode>().As<IInternalPluginHost>().ExternallyOwned();
  6.         cb.RegisterType<AutoFacIOFactory>().As<AutoFacIOFactory, IIOFactory>().InstancePerLifetimeScope();
  7.         cb.RegisterType(pluginType).As<IPluginEvaluate>().InstancePerLifetimeScope();
  8.     }
  9. );
  10.  
  11. this.iofactory = container.Resolve<AutoFacIOFactory>();
  12. this.PluginBase = container.Resolve<IPluginEvaluate>();
  13. autoevaluate = nodeInfo.AutoEvaluate;
  14. iofactory.OnCreated(EventArgs.Empty);

Also really simple, and can be even more simplified, but I wanted to do a quick try.

Best of all now, I have proper inheritance support, keeping what I want private/protected and so on:

Code Snippet
  1. [PluginInfo(Name = "CopyCounter", Category = "DX11.Buffer", Version = "", Author = "vux")]
  2. public class CopyCounterNode : IPluginEvaluate, IDxResourceContextProvider
  3. {
  4.     private Pin<DX11Resource<DX11StructuredBuffer>> FInBuffer;
  5.     private ISpread<DX11Resource<DX11RawBuffer>> FOutBuffer;
  6.  
  7.     protected RenderDevice Device { get; private set; }
  8.  
  9.     public CopyCounterNode(RenderDevice device, IIOFactory iofactory)
  10.     {
  11.         this.Device = device;
  12.         this.FInBuffer = iofactory.CreateResourceInputPin<DX11StructuredBuffer>("Buffer In",true);
  13.         this.FOutBuffer = iofactory.CreateResourceOutputPin<DX11RawBuffer>("Buffer Out",true);
  14.         this.FOutBuffer[0] = new DX11Resource<DX11RawBuffer>();
  15.     }
  16. }
  17.  
  18. [PluginInfo(Name = "CopyCounter2", Category = "DX11.Buffer", Version = "", Author = "vux")]
  19. public class CopyCounterNode2 : CopyCounterNode
  20. {
  21.     private ISpread<DX11Resource<DX11RawBuffer>> FOutBuffer2;
  22.  
  23.     public CopyCounterNode2(RenderDevice device, IIOFactory iofactory) : base(device,iofactory)
  24.     {
  25.         this.FOutBuffer2 = iofactory.CreateResourceOutputPin<DX11RawBuffer>("Buffer Out",true);
  26.         this.FOutBuffer2[0] = new DX11Resource<DX11RawBuffer>();
  27.     }
  28. }

No more references to MEF, inheritance is streamlined. Also now you can see that device can't be written by then child class, and no more compiler warning.

Technically class is now perfectly isolated, and node don't know about autofac or using a container. The IOC manages it for you, you can replace it by another one if you want, or make you own barebone version.

Now you also have proper initialization at the place it should be : the constructor.

And as usual one very funny thing, this is MUCH less code than the .NET factory for the same features ;)

No comments:

Post a Comment