Migration guide for rendering between Prysm versions

Transitioning from Prysm 1.1 to 1.2

This section outlines the changes you need to apply in order to upgrade from Prysm versions 1.1 and prior to 1.2 and later.

The current backend API was designed around DirectX 11 and therefore implementing a DirectX 11 backend with the current API is simple and intuitive. However, with the advent of modern low-level APIs (e.g. Dx12, Metal and Vulkan) and concepts such as render passes, enhancements of the backend API were needed, so that the backend implementation for those graphics APIs is easier and more efficient.

You can read more about the motivation for the backend API changes here.

In Prysm 1.2, Renoir's backend API has been modified significantly with the introduction of 2 new commands, 4 new Renoir Core capabilities and one new shader type. There are also several changes in the graphics backend that must be adapted from previous versions in order for the new version to work.

If you prefer to make minimal changes to you backend and not use the new capabilities, here are the steps for migrating:

  • Add the following lines to the FillCaps method
    outCaps.ShouldUseRenderPasses = false;
    outCaps.ShouldClearRTWithClearQuad = false;
    outCaps.ConstantBufferBlocksCount = 1;
    outCaps.ConstantBufferRingSize = 1;
    outCaps.ShaderMapping[ST_ClearQuad] = ST_ClearQuad;
  • Add unsigned size as last argument of the CreateConstantBuffer method and use it for the constant buffer allocation size. This is how the CreateConstantBuffer method declaration in the backend header should look like:
    virtual bool CreateConstantBuffer(CBType type, ConstantBufferObject object, unsigned size) override;
    Example of using the size parameter on constant buffer creation from the DirectX11 backend:
    bool Dx11Backend::CreateConstantBuffer(CBType, ConstantBufferObject object, unsigned size)
    D3D11_BUFFER_DESC bufferDesc;
    bufferDesc.ByteWidth = size;
    // Create the constant buffer with the filled buffer description
    m_Device->CreateBuffer(&bufferDesc, ..)
  • In SetRenderTarget stop using EnableColorWrites flag as it is no longer present and instead handle the PipelineState's ColorMask field in CreatePipelineState. This field currently only supports the values ColorWriteMask:CWM_None and ColorWriteMask::CWM_All, which correspond to the previous false and true values of EnableColorWrites. Set the appropriate value of the graphics API's render target color write mask. E.g. in DirectX11 the write mask is placed in the description of the blend state:
    bool Dx11Backend::CreatePipelineState(const PipelineState& state, PipelineStateObject object)
    D3D11_BLEND_DESC desc;
    desc.RenderTarget[0].RenderTargetWriteMask = UINT8(state.ColorMask);
    // Create the blend state with the filled description
    m_Device->CreateBlendState(&desc, ...)
  • In ExecuteRendering add empty cases with only break in them for BC_BeginRenderPass and BC_EndRenderPass:
    case BC_BeginRenderPass:
    case BC_EndRenderPass:

The MSAASamples field was added to the PipelineState structure, so you may start using it in CreatePipelineState.

Below we will describe each new capability, how it can affect you backend and what are the needed changes you need to make to use it.

When the ShouldUseRenderPasses capability is enabled, then Renoir starts enqueuing the commands BeginRenderPass and EndRenderPass and stops issuing the SetRenderTarget and ResolveRenderTarget commands. The BeginRenderPass command provides all the needed information for starting a render pass in modern graphics APIs like Metal and Vulkan. This information includes the render targets, whether they should be cleared on render pass load and if they should be resolved on store. Here are the additional steps you need to make to start using this capability:

  • Set ShouldUseRenderPasses to true in the FillCaps method
  • You can remove the implementation of the SetRenderTarget and ResolveRenderTarget methods and add an assert that they are never called
  • Implement a BeginRenderPass method, which handles the corresponding command by using the provided information by it to begin a render pass in the graphics API
  • Implement a EndRenderPass method, which handles the corresponding command by ending the current render pass and possibly also resetting any currently kept state of the render pass. E.g. in our Metal backend the implementation of the EndRenderPass method is the following:
    [m_State->CurrentCmdEncoder endEncoding];
    m_State->CurrentCmdEncoder = nil;
    m_State->BoundGPUState = GPUState();

Enabling the ShouldClearRTWithClearQuad capability will make Renoir issue fullscreen clear quad instead of calling ClearRenderTarget. The clear quad is done through a new vertex and pixel shader. The capability was added so that we don't need to create a new render pass to clear a render target in graphics APIs like Metal, which do not provide an easier way to do it. Here are the additional steps you need to make to start using this capability:

  • Set ShouldClearRTWithClearQuad to true in the FillCaps method
  • You can remove the implementation of the ClearRenderTarget method and add an assert that it is never called
  • You need to create a new ST_ClearQuad vertex and pixel shader, compile them if necessary and start using them. You can check out the example ST_ClearQuad HLSL shaders provided with the DirectX11 backend. The Metal backend is using the clear quad capability, so you can check out how to use the new shaders in its implementation.

The ConstantBufferRingSize capability allows you to set the size of the internal ring buffer, which is used to manage Renoir's constant buffers. We recommend to set this size to 4 for low-level graphics APIs like Dx12, Metal and Vulkan. The motivation for this particular size is that the maximum count of buffered frames in a standard pipeline is three and in order to surely avoid overlap of constant buffers, they should be managed by a circular buffer with size 4. If you have a pipeline with higher maximum count of buffered frames, then this value should be changed accordingly. For most high-level graphics APIs ring buffer size should be set to 1, because the drivers for them handle constant buffer overlap internally and therefore a greater value for the ring buffer size is unnecessary.

The only steps you need to make to start using this capability are:

  • Set ConstantBufferRingSize to the appropriate value in the FillCaps method
  • If you have a ring buffer for the constant buffers in your backend, then you can remove it, because Renoir will do it automatically for you

The ConstantBufferBlocksCount capability allow you to set the count of aligned constant buffer blocks for each constant buffer type. Renoir will issue a CreateConstantBuffer call with size equal to (constant buffer blocks count) * (aligned specific constant buffer size) for each constant buffer type. If the blocks count value is greater than 1, then if the regular constant buffer becomes full, Renoir will make sure that a new auxiliary constant buffer is allocated. If the blocks count value is equal to 1, then Renoir won't create any auxiliary constant buffers. Auxiliary constant buffers are allocated per frame, thus being allocated before ExecuteRendering is called and deallocated immediately after that. Setting constant buffer ring size and blocks count value to greater than 1 usually goes hand in hand, because both provide functionality that otherwise should be explicitly implemented in the backend for low-level graphics APIs like Dx12, Metal and Vulkan. For other APIs that don't support constant buffers, but use uniform slots (e.g OpenGL) both capabilities should be set to 1 in order to avoid unnecessary constant buffer creation.

The only steps you need to make to start using this capability are:

  • Set ConstantBufferBlocksCount to the appropriate value in the FillCaps method
  • Remove all the logic in your backend, which manually creates auxiliary buffers once the regular ones are full. Renoir will create and manage them automatically