Direct2D: How to implement deferred registration of custom effects

One weird trick …

If you’re writing a custom effect for Direct2D, which involves implementing the ID2D1EffectImpl interface, you may run into a chicken-and-egg problem if you’re trying to use another custom effect that isn’t yet registered with the Direct2D factory.

Direct2D is set up very carefully to prevent the custom effect from getting access to things like the ID2D1DeviceContext. Unfortunately, you also don’t get access to the ID2D1Factory1, and this creates a brick wall that blocks you from registering effects just-in-time (versus ahead-of-time).

When Direct2D calls your ID2D1EffectImpl::Initialize() method, it’ll provide you with an ID2D1EffectContext and an ID2D1TransformGraph. The ID2D1EffectContext::CreateEffect() method allows you to instantiate another effect, which you can then use in a call to ID2D1EffectContext::CreateTransformNodeFromEffect() to create an ID2D1TransformNode that you can place into your transform graph.

However, there are no registration methods on ID2D1EffectContext like you see on ID2D1Factory1. This means that you have to register your effects up-front, probably in the custom effect’s static Register method (which is the pattern used in the Direct2D documentation).

Notably, this requires that your custom effect has what amounts to a dependency manifest in its Register method whereby it calls all of the Register methods for the custom effects that it needs to utilize later. This can be seen as a violation of the DRY (“Don’t Repeat Yourself") principle, and can be a source of subtle bugs and frustrating debugging if it gets out of date.

However, you can still implement a proper deferred registration mechanism. Here’s how I do it in Paint.NET, although I won’t show the actual code because it’s in C# with lots of my own custom things that won’t make sense out-of-context:

Step 1: Implement your custom effect all the way up through ID2D1EffectImpl::Initialize(). Note that this method receives an ID2D1EffectContext and an ID2D1TransformGraph.

At this point you’ll be wanting to use ID2D1EffectContext::CreateEffect(), followed by ID2D1EffectContext::CreateTransformNodeFromEffect(), and then an appropriate method on the ID2D1TransformGraph to add the effect’s transform node into your transform graph.

Step 2: If ID2D1EffectContext::CreateEffect() returns an error, then either the effect can’t be created or it hasn’t yet been registered. If it didn’t return an error, then go about your business normally. Otherwise, proceed to step 3.

NOTE: Direct2D is supposed to return D2DERR_EFFECT_IS_NOT_REGISTERED (0x88990028), but it doesn’t. It’ll actually return HRESULT_FROM_WIN32(ERROR_NOT_FOUND), which is 0x80070490. I check for both, just in case Microsoft decides to fix its code.

Step 3: At this point you’re normally SOL, as we need to register the other custom effect we want to use, but we don’t have an ID2D1Factory1 to call a RegisterEffect method on.

So, step 3 is to actually create an effect that does exist using ID2D1EffectContext::CreateEffect(). I use CLSID_D2D1Flood, as it’s a very simple effect and probably cheap. We won’t be using the effect for its intended purpose – it’s a tool to get what we really want.

Step 4: Now that you have an ID2D1Effect, we’re still not quite where we want to be because it doesn’t implement ID2D1Resource which has the GetFactory() method that we need to get the ID2D1Factory that has the RegisterEffect methods we need. If only there were a way …

But we’re not stuck yet! Either call ID2D1Effect::GetOutput() to get the effect’s ID2D1Image, or use QueryInterface to retrieve a pointer to the ID2D1Image interface. The documentation for GetOutput establishes these as equivalent, btw.

Step 5: Now you can call ID2D1Resource::GetFactory() on the effect’s ID2D1Image pointer, and then do the obvious thing … (e.g. QI for ID2D1Factory1, then use that for registering the other effect). I like to call ID2D1Factory1::GetEffectProperties() first to try and distinguish between the effect not being registered versus the effect’s initialization returning an error. If the effect is already registered then just bail and return a failure HRESULT instead of trying to forge ahead with re-registering the effect.

Step 6: Don’t forget to release all of the temporary objects.

An important thing here is that effect registration works correctly when called from within the initialization of another effect. Hopefully it stays that way!

Anyway, this was one weird trick I found that enabled a lot of my code to be simpler. It’s still better if you can find a different way to provide access to the ID2D1Factory1 for your custom effect, but this is a good fallback technique.

In an upcoming update, Paint.NET will be opening the doors for effect plugins to use Direct2D, to implement custom Direct2D effects, to chain these effects together, and to not have to worry about up-front registration of custom effect dependencies. Stay tuned!


2 thoughts on “Direct2D: How to implement deferred registration of custom effects

  1. Leo Feret says:

    Explaining Direct2D implementation intricacies is just another reason for a huge tip of the hat to Rick.

Comments are closed.