Resetting States In A Data-Driven Renderer

Started by
5 comments, last by Jason Z 7 years, 8 months ago

I was reading about data driven renderer architecture and it was states that you'd submit a "drawcall" that has all it's necessary states selfcontained. But they also stated that you'd reset all states back to null afterwards so that no conflict remains.

I'm currently wondering if that makes sense from a performance sense, since flushing everything only to "re-set" it again in the next drawcall seems like a lot of wasted state switches. Right now I'm leaning more towards leaving the states as they are after the drawcall which might be more error prone but has no wasted state switching.

However I did have some issues with state remnants before and it can be a real nuisance to debug sometimes...

Any suggestions ?

Advertisement

I was reading about data driven renderer architecture and it was states that you'd submit a "drawcall" that has all it's necessary states selfcontained. But they also stated that you'd reset all states back to null afterwards so that no conflict remains.

Do they? I mean, both read about and implemented a similar renderer, but I don't remember reading about, nor did I actually reset all states to null after that call.

What they are probably talking about is that each drawcall should contain all possibly state settings, and those that are not specified explicitely ie. by the mesh/material are set to a default value. That still means that you will leave everything up after every drawcall, but since the next drawcall has all the necessary states, you can just set the states that actually changed (using redundant state checking). At least thats how I've read it and done it myself, and I also think resetting all states on the GPU-side after the draw-call is a pretty bad idea (if you want to keep the implementation simple, you can skip the redundant state checking and just set everything again for each draw - the API/GPU will already optimize the redundant calls, though at some cost at the CPU-side).

For DX11 I wrote RAII wrappers like this:


class OmBlendState
{
public:
    //blendFactor == nullptr => DX will use{ 1, 1, 1, 1 }
    OmBlendState(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11BlendState* blendState, const float* blendFactor = nullptr, unsigned sampleMask = 0xFFFFFFFF);
    ~OmBlendState();

private:
    const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& m_context;
};

OmBlendState::OmBlendState(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11BlendState* blendState, const float* blendFactor /*=nullptr*/, unsigned sampleMask /*=0xFFFFFFFF*/)
    : m_context{context}
{
    context->OMSetBlendState(blendState, blendFactor, sampleMask);
}

OmBlendState::~OmBlendState()
{
    m_context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

Usage:


void TranspPass::Render()
{
    ...
    OmBlendState bs{ m_context, m_omAlphaBlendState.Get() };
    ...
    m_context->DrawIndexedInstanced(iaBuff.GetIndexCount(), static_cast<UINT>(instData.size()), 0, 0, 0);
}

Wrappers upon other states have different interface, for example RT:


class OmRenderTargets
{
public:
	//Max RTS - 8 (DX11 && DX12)
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11RenderTargetView* rtv2, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11RenderTargetView* rtv2, ID3D11RenderTargetView* rtv3, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11RenderTargetView* rtv2, ID3D11RenderTargetView* rtv3, ID3D11RenderTargetView* rtv4, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11RenderTargetView* rtv2, ID3D11RenderTargetView* rtv3, ID3D11RenderTargetView* rtv4, ID3D11RenderTargetView* rtv5, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11RenderTargetView* rtv2, ID3D11RenderTargetView* rtv3, ID3D11RenderTargetView* rtv4, ID3D11RenderTargetView* rtv5, ID3D11RenderTargetView* rtv6, ID3D11DepthStencilView* pDepthStencilView);
	OmRenderTargets(const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& context, ID3D11RenderTargetView* rtv0, ID3D11RenderTargetView* rtv1, ID3D11RenderTargetView* rtv2, ID3D11RenderTargetView* rtv3, ID3D11RenderTargetView* rtv4, ID3D11RenderTargetView* rtv5, ID3D11RenderTargetView* rtv6, ID3D11RenderTargetView* rtv7, ID3D11DepthStencilView* pDepthStencilView);

	~OmRenderTargets();

private:
	const Microsoft::WRL::ComPtr<ID3D11DeviceContext>& m_context;
};

I do not use RAII wrappers upon Vertex Shader, Pixel Shader, IA, because it will be rewritten on next rendering stage.

But for GS/CS I always do.

So after each Rendering stage, pipeline is pretty clean and there is no need to reset states I did not touch.

This solution is better than having some "Reset all states" object.

As a bonus - Exception safe :)

I never reset states.

Resetting states at the end of a rendering stage implies that the next rendering stage can make assumptions about what states are when going into it. That seems very very dangerous. Instead I have each rendering stage set all states required on entry. If required I can easily add state filtering to this, so that only states which actually change are set; otherwise this kind of setup also behaves itself properly with deferred contexts in D3D11.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

I was reading about data driven renderer architecture and it was states that you'd submit a "drawcall" that has all it's necessary states selfcontained. But they also stated that you'd reset all states back to null afterwards so that no conflict remains.

If a "drawcall" has all it's necessary states self-contained, then this is a "stateless" rendering architecture (compared with the underlying D3D/GL API, which is very much a "state machine"). In a stateless rendering architecture, there's no need to reset states, as the next "drawcall" will contain all the necessary states too...

Thanks for the answers, that made it clear :)

I never reset states.

Resetting states at the end of a rendering stage implies that the next rendering stage can make assumptions about what states are when going into it. That seems very very dangerous. Instead I have each rendering stage set all states required on entry. If required I can easily add state filtering to this, so that only states which actually change are set; otherwise this kind of setup also behaves itself properly with deferred contexts in D3D11.

This! I wrote a small blog post a while back about state monitoring, and you can check it out in Hieroglyph if you want to see a sample implementation. Don't let your draw calls make any assumptions (it may even be a good idea to do testing where you intentionally set weirdo states...) and you will be happier when you start using deferred contexts and/or multithreaded draw call submission.

This topic is closed to new replies.

Advertisement