James Stanard, Silicon, Graphics & Media DEV R&D
Updated January 30th, 2018
When implementing the checkerboard rendering technique (CBR), most of the effort is in carefully modifying your pixel shaders. As of June 2017 XDK QFE2, we have provided a compiler feature to automatically modify your shaders to support CBR. This allows you to quickly adopt CBR, while also making it easier to revert. It is also possible to disable these changes at runtime without recompiling your shaders, though this adversely affects performance and should only be used during development.
There are various MSAA configurations that might be used with CBR: 4xMSAA “square” or 2xMSAA “rectangle” layouts. We have assumed that you will use the horizontal rectangle layout. Render targets should be half as wide as non-CBR render targets but with two samples per pixel. We expect you will use 1f2s EQAA to reduce color storage.
This compiler feature does not introduce a “Triangle ID” feature to your shaders. Nor are strategies applied for alpha testing, which requires testing all samples. For more information please refer to the other CBR whitepaper, Checkerboard Rendering on Xbox One.
CBR works by rendering half of the pixels on even frames and the other half on odd frames, repeating. To achieve this effect, MSAA is used in an unconventional way. To reduce shading by half, pixels are paired in the form of 2xMSAA. The default sample layout is not a regular grid, so we reassign the sample positions to be in the center of the left and right halves of the pixel. We enable per-sample shading so that each sample is shaded at its position (i.e. the vertex attributes are interpolated at the sample position). Then we limit shader sample iteration to one (‘0’ in log2 units) so that only the first sample is shaded.
Sample positions can be programmed independently for each corner of the pixel quad. We use this to form an alternating checkerboard pattern. As a convention, we recommend even frames render the pixel at (0, 0). In pseudo-code, this is how we assign the positions of sample 0 and 1.
TopLeftX[0] = TopRightX[0] = BottomLeftX[1] = BottomRightX[1] = FrameIsOdd ? +4 : -4;
TopLeftX[1] = TopRightX[1] = BottomLeftX[0] = BottomRightX[0] = FrameIsOdd ? -4 : +4;
Because the samples are staggered and skewed, texture coordinate gradients are deformed. The gradients need to be corrected differently depending on whether the frame is odd or even. The formula is as follows:
float2 DDX = 0.5 * ddx(UV);
float2 DDY = ddy(UV) + FrameIsOdd ? +DDX : -DDX;
Because the gradients must be corrected, all texture sampling that implicitly or explicitly uses gradients must be replaced with explicit gradient-based sample instructions and use the corrected gradients. The list of affected HLSL methods follows:
Sample()
SampleBias()
SampleCmp()
ddx*()
ddy*()
Those that are unaffected are:
SampleGrad() (note that the gradients may be affected)
SampleLevel()
SampleCmpLevelZero()
Gather*()
Optionally, the compiler can also automatically double the value of SV_Position.x. Due to CBR viewports being half-width, SV_Position.x has half the range of non-CBR rendering. Some screen-space texture lookups may need to be relative to the full viewport dimensions. If you need screen-space coordinates to be consistent with non-CBR coordinates, there is an option to automatically double SV_Position.x
To direct the shader compiler to enable auto-checkerboarding, define this macro. This macro also determines the constant buffer and offset where you will provide the “FrameIsOdd” bit.
#define __XBOX_AUTO_CBR "CBV( cbv_register, [cbv_space,] word_offset )"
For example:
#define __XBOX_AUTO_CBR "CBV( b1, space20, 7 )"
Specifies that CBR should be auto-enabled and the parity bit can be found at the constant buffer bound at register(b1, space20) with a 28-byte offset (7 * 4 bytes). (As of the current version of this feature, this must be a root CBV, i.e. not in a descriptor heap and not a root constant.) The bit is always the LSB of the quadword. Note that the namespace is optional. If omitted, it defaults to space0 and is compliant with Shader Model 5.0.
To enable automatic doubling of SV_Position.x to be consistent with non-CBR rendering, define:
#define __XBOX_AUTO_CBR_ENABLE_SV_POSITION_X_MUL_BY_2
To be able to toggle CBR mode without recompiling shaders, you must also define:
#define __XBOX_AUTO_CBR_DYNAMIC_MODE
This directive increases shader complexity because modifications must test another bit to determine if the corrected or original gradients are to be used and whether to scale SV_Position.x. This bit is adjacent to the LSB. A ‘1’ indicates that CBR should be disabled.
Enabling CBR in a shader has a performance cost in the form of extra instructions, higher register usage, and more expensive SampleGrad texture fetches. But remember that you will use about half as many pixel shader threads. As a back of the envelope calculation, let’s assume an added 10% overhead cost of adding CBR support to your shader. With 110% cost multiplied by 50% threads, we should expect about 55% of the total execution time for pixel shaders alone. However, vertex shader work remains unaffected, and the amount of texture memory read should also remain unaffected. We have seen CBR rendering times at about 60% of non-CBR (for affected render passes), which seems in line with expectations.
With the dynamic mode support, let’s bump the overhead cost estimate to about 12%, which is minor but undesirable in a shipping title. This would inflate CBR passes to 61% of non-CBR rendering. But if you use the same shader for non-CBR rendering (by setting the ‘disable’ bit), you can expect your shading passes to reach 112%–a tough pill to swallow. We do not recommend shipping a game with shaders compiled with __XBOX_AUTO_CBR_DYNAMIC_MODE. But you should measure the difference in your game, as your bottlenecks might be different.
Due to the added overhead of gradient calculations, we advise that you replace Sample() with SampleLevel() when it is known that the texture does not have mip-maps. Likewise, for SampleCmp() with shadow maps, you should substitute SampleCmpLevelZero(). Neither form is incorrect and will yield the same visual results, but the compiler will no longer be forced to correct gradients when they have no effect.
With our shader compiler you can more easily implement CBR in your game engine. We also facilitate having both CBR and non-CBR rendering paths, so you can compare performance and visual quality. Not everything is done for you, but arguably the hardest part can be avoided with some shader compiler directives. Your shaders will remain unchanged, maintaining their readability and correctness. We expect shaders compiled for CBR to be longer and a little slower, but by following the guidelines laid out in this document you can avoid unnecessary performance degradation.