Screen-space Variable Rate Shading in Unity with DirectX 12.
--
Very often different game projects have perfomance issues around pixel fillrate on GPU. If profiling tools show you that it’s an actual bottleneck, one of the ways you can follow to optimize is to render you game at lower resolution. It’s a simple approach but the quality of your image can suffer dramatically.
Variable Rate Shading is a modern technique that allows you to have a-la “different resolutions” in different regions of your frame buffer. This feature can make you render actually flexible: assume you have region of screen that’s covered by HUD elements, or blurred via some post-processing effect. These are examples of cases when you can render some parts of environment at lower resolution without noticable quality loss in render target.
Let’s look into VRS in DirectX 12. Depend on Tier your graphics adapter supports you have mix of options to control shading rate:
- Set shading rate globally (per-draw mode): so you can do something like that:
...
_cmdList->RSSetShadingRate(D3D12_SHADING_RATE_1X1, nullptr);
//draw something
_cmdList->RSSetShadingRate(D3D12_SHADING_RATE_4X4, nullptr);
//draw something
...
- Set shading rate over your mesh via SV_ShadingRate attribute in vertex and geometry shaders (per-primitive contribution).
- Set shading rate in screen space via special image (VRS-mask).
So let’s try to implement the third option in Unity.
- First of all we should notice that resolution of VRS-mask is not equal to resolution of render target. From d3d12 specs we can see that render-target is divided into multiple square tiles and you should define shading rate per tile.
- Let’s check tile size:
device->CheckFeatureSupport(
D3D12_FEATURE_D3D12_OPTIONS6,
&_featureOptions,
sizeof(_featureOptions));
//...
int RenderAPI_D3D12::GetVRSTileSize()
{
return _featureOptions.ShadingRateImageTileSize;
}
- VRS-mask initialization… It’s important to have DXGI_FORMAT_R8_UINT here:
D3D12_RESOURCE_DESC vrsMaskResDesc = {};
vrsMaskResDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
vrsMaskResDesc.Alignment = 0;
vrsMaskResDesc.Width = sizeW;
vrsMaskResDesc.Height = sizeH;
vrsMaskResDesc.DepthOrArraySize = 1;
vrsMaskResDesc.MipLevels = 1;
vrsMaskResDesc.Format = DXGI_FORMAT::DXGI_FORMAT_R8_UINT;
vrsMaskResDesc.SampleDesc.Count = 1;
vrsMaskResDesc.SampleDesc.Quality = 0;
vrsMaskResDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
vrsMaskResDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
device->CreateCommittedResource(
&defHeapTypeProps,
D3D12_HEAP_FLAG_NONE,
&vrsMaskResDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&_vrsMaskRes));
- For this test the only thing we do is to set left half of the screen in low-resolution mode and to set right part of the screen in high-resolution mode
auto vrsTransition = CD3DX12_RESOURCE_BARRIER::Transition(_vrsMaskRes,
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_SHADING_RATE_SOURCE);
std::vector<BYTE> data(sizeW * sizeH);
for (auto i = 0; i < sizeH; i++)
{
for (auto j = 0; j < sizeW; j++)
{
data[sizeW * i + j] = (j >= sizeW / 2) ? D3D12_SHADING_RATE_1X1 : D3D12_SHADING_RATE_4X4;
}
}
auto elementByteSize = 1; //single byte (R8_UINT format)
_vrsMaskRes->WriteToSubresource(0, nullptr, data.data(), elementByteSize * sizeW, elementByteSize * sizeW * sizeH);
_cmdAllocator->Reset();
_cmdList->Reset(_cmdAllocator, nullptr);
_cmdList->ResourceBarrier(1, &vrsTransition);
_cmdList->Close();
ID3D12CommandList* cmdsLists[] = { _cmdList };
_queue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
_fenceValue++;
_queue->Signal(_fence, _fenceValue);
Wait();
Log("vrs mask created");
- Combine variable shading rate in command list:
_cmdList->RSSetShadingRateImage(_vrsMaskRes);
D3D12_SHADING_RATE_COMBINER combiners[] = {
D3D12_SHADING_RATE_COMBINER::D3D12_SHADING_RATE_COMBINER_MAX ,
D3D12_SHADING_RATE_COMBINER::D3D12_SHADING_RATE_COMBINER_MAX
};
_cmdList->RSSetShadingRate(D3D12_SHADING_RATE_1X1, combiners);
- Result of rendering in Unity editor will be:
Source code for sample: https://bitbucket.org/maxpushkarev/vrs/src/master/