Smart Pointers (shared_ptr and ComPtr)

Started by
28 comments, last by Happy SDE 7 years ago

I have multiple classes/structs owning and/or using for instance ID3D11Device and ID3D11DeviceContext. Currently I use ComPtr smart pointers for these structures instead of raw pointers in case a class/struct needs to own them and a reference otherwise. Generally, however, my Renderer class is the real owner. If my Renderer is destructed, owning a ID3D11Device or ID3D11DeviceContext is useless since rendering is finished. Is it therefore better practice to always pass references around and use member variable references in case a class needs to 'own' a ID3D11Device and/or ID3D11DeviceContext? So ComPtr will only be used in my Renderer? It should even be possible to use the more efficient (since no reference counting is required) unique_ptr with a custom destructor making ComPtr useless.

🧙

Advertisement

My engine follows next guidelines:

1. If something owns COM object, it is wrapped over Microsoft::WRL::ComPtr

Example: objects, that represent passes on CPU side, own: shaders, input layouts, constant buffers, ...

Destructor of a pass will release owned object.

2. If something ONLY uses a COM object, it will receive it as a raw pointer:

void EnvMapPass::Render(const RdCam& rdCam, ID3D11RenderTargetView* rtv, ID3D11DepthStencilView* dsv)

It assumes, that I give a guarantee, that the owner will outlive a user.

Benefit: not much AddRef/Release() with no leaks

Also with this approach it is possible to store raw pointers in a user (like DeviceContext in a Pass object), BUT you need provide guarantee, that the user-object will live less, than an owner (like my class "MGL::Device", that actually owns the context)

Destructor of a pass will release owned object.
Do you mean an explicit reset or just the default behavior when releasing the stack space (i.e. calling destructor on ComPtr)?

it will receive it as a raw pointer

But now you need to provide guarantee of not passing nullptr. Isn't this a bit odd for ID3D11Device and ID3D11DeviceContext, since you "never" expect them to be nullptr.

🧙

Happy SDE, on 27 Mar 2017 - 9:08 PM, said: Destructor of a pass will release owned object. Do you mean an explicit reset or just the default behavior when releasing the stack space (i.e. calling destructor on ComPtr)?

Renderer owns a Pass.

Renderer is wrapped by std::unique_ptr.

Pass is a Renderer's member (stack allocation, not by new).

When I need release renderer, I call std::unique_ptr::reset().

Pass will automatically destroyed with it's all owned objects (like input layout) via RAII (stack destructor, +ComPtr::Release() for owned objects).

Device lives longer, than renderer (in current implementation, it is a singleton).

it will receive it as a raw pointer

But now you need to provide guarantee of not passing nullptr. Isn't this a bit odd for ID3D11Device and ID3D11DeviceContext, since you "never" expect them to be nullptr.

Usually textures are created once (between resizes), context is created once before Renderer.

If something goes wrong, exception will be thrown and there will be no way of calling Render() in this case.

So I have 146% guarantee, that there will be no nullptr at Render() or similar call. :)

It's called an "invariant" in C++.

input layout

Is your input layout owned by multiple objects? If not, aren't there a lot of ComPtrs with a ref count of 1 and isn't this an overhead compared to unique_ptr with a custom destructor?

Device lives longer, than renderer (in current implementation, it is a singleton).

If your Device outlives most of your code, why using the same smart pointers. I mean if objects A, B, C all have a ComPtr to the same data, but without A (e.g. your Device), B and C should not exist since no rendering will be performed anymore. The problem with a ComPtr is that your pointed data is not guarenteed to be destructed after destructing A (without using reset) since B and C could still live due to programmers violating the design?

🧙

If IL is owned by multiple Passes, there will be AddRef() twice, like standard rules of c++.

This AddRef() will be called only 2 times.

The other option: have SharedObjects object, that will own the IL, and pass raw IL pointer to clients.

IF you pass ComPtr by value in each Render() call, this is a problem.

But if you only store it as a member - no overhead.

shared_ptr is the same as AddRef(): ++ operator is atomic.

Situation when more than 1 object owns the same object is quite rare (in my code).

Usually the construction is multi-level:

Device -> Renderer -> Pass.

For GameRenderer, I know what passes are there, and their order.

For SplashScreenRenderer, passes are different, but they are known.

It is static on compilation time.

Customization is available in construction time (in constructors), for example: MSAA/no MSAA version of a shader.

The other customization option have 2 version of a renderer with the same interface:

Renderer::std::unique_ptr<ISsao> m_ssao;

And have 2 implementation of it: NoSsaoPass and SsaoPass.

NoSsaoPass will do nothing.

But after all is created, there is no need to destroy any pass/any of it subobject.

There is only need to update buffers/call Render() in some (strict) order.

This is a job of a particular renderer (like SplashScreenRenderer or GameRenderer).

When a game is finished, entire renderer is destroyed (with all Passes, and it's subobjects), and new renderer (SpashScreenRenderer will be created).

The device and context will be still alive.

Situation when more than 1 object owns the same object is quite rare (in my code).

So no ComPtrs in all these cases?

🧙

Option1:


class Renderer{
   Renderer(): m_passA{m_shared.m_sharedIL.Get()}, m_passB{m_shared.m_sharedIL.Get()}{}
   
   SharedObjects m_shared; //Should be before m_passA and m_passB
   PassA         m_passA;
   PassB         m_passB;
}
struct SharedObjects
{
    SharedObjects() {build m_sharedIL, if something went wrong, throw exception}
    ComPtr<IL> m_sharedIL;
}
class PassA
{
    PassA(IL* il) : m_il{il}{}
    IL* m_il;
}
class PassB
{
    PassB(IL* il) : m_il{il}{}
    IL* m_il;
}

Option2:

No SharedObjects class, but PassA and PassB will store ComPtr to the same object;

Option1:


class Renderer{
   Renderer(): m_a{m_shared.m_sharedIL}, m_b{m_shared.m_sharedIL}{}
   
   SharedObjects m_shared; //Should be before m_a and m_b
   PassA m_a;
   PassB m_b;
}
struct SharedObjects
{
    ComPtr<IL> m_sharedIL;
}
class PassA
{
    PassA(IL* il) : m_il{il}{}
    IL* m_il;
}
class PassB
{
    PassB(IL* il) : m_il{il}{}
    IL* m_il;
}

Option2:

No SharedObjects class, but PassA and PassB will store ComPtr to the same object;

Option 3 : if you use IL& instead of IL* in PassA and PassB, you could use unique_ptr<IL> with a custom deleter?

🧙

Option 3 : if you use IL& instead of IL* in PassA and PassB, you could use unique_ptr with a custom deleter?

How many D3D objects do you have?

(how much bytes are you going to save?)

This topic is closed to new replies.

Advertisement