Graphics State
Implementation status: all four state-object families are present, with built-in presets and custom state creation, and the state types themselves are well covered by CNA's Graphics unit tests. What varies is whether the selected renderer honours a given state. The notable gaps: DIRECTX12 has no scissor, no viewport, no stencil and no blend factor, and WebGPU's stencil is non-functional. Read the per-section notes and Renderers before designing around any state you cannot see take effect.
Do not treat a positive SupportsCapability() answer as proof that a state is honoured. The base returns false for multi-stream input and compiled effects and delegates stencil, but returns true for its other entries; DIRECTX9, DIRECTX11, DIRECTX12 and SDL_GPU never override it. Several claims therefore fail open. Verify visually or with a pixel test.
Overview
All render state in CNA follows the XNA 4.0 immutable state-object pattern. Rather than toggling individual render states through a stateful device API, you create a state object (either a built-in preset or a custom instance), then assign it to the appropriate property on GraphicsDevice before issuing draw calls.
The four state-object types and where they live on GraphicsDevice:
graphicsDevice.BlendState— controls how incoming fragment colours are blended with the render targetgraphicsDevice.DepthStencilState— controls depth testing, depth writing, and stencil operationsgraphicsDevice.RasterizerState— controls face culling, fill mode, and scissor/depth biasgraphicsDevice.SamplerStates[n]— per-slot texture filtering and address modes (up to 16 slots)
State assigned to GraphicsDevice applies to every draw call that follows until you change it again. A common mistake is forgetting to reset state between rendering passes — for example, leaving BlendState::Additive active when starting the opaque geometry pass. Always set state explicitly at the beginning of each logical pass.
Always reset state after each pass. State is persistent — it does not reset automatically between frames or between draw calls. Set the appropriate preset at the start of every opaque pass, every transparent pass, and every 2D/HUD pass.
BlendState
BlendState describes how the output of a pixel shader (the source) is combined with the existing colour already in the render target (the destination). CNA provides four built-in presets that cover the most common use cases.
Built-in presets
| Preset | Use case |
|---|---|
BlendState::Opaque |
Fully opaque geometry — blending is disabled entirely. The fastest blend mode; use it for all solid meshes. |
BlendState::AlphaBlend |
Standard transparency using premultiplied alpha. This is the default for SpriteBatch and the correct choice for textures exported from tools that premultiply alpha at export time. |
BlendState::NonPremultiplied |
Straight (non-premultiplied) alpha. Use when loading PNG files whose RGB channels have not been premultiplied by the alpha value — a common default for art tools that export plain PNG. |
BlendState::Additive |
Additive (light-accumulating) blending. Source colour is added directly to the destination without obscuring it. Ideal for particle effects, glow, fire, and laser beams. |
Setting BlendState
// Use a built-in preset
graphicsDevice.BlendState = BlendState::AlphaBlend;
// Draw transparent geometry...
// Reset to opaque for the next pass
graphicsDevice.BlendState = BlendState::Opaque;
Custom BlendState
When the presets are not sufficient, create a BlendState and configure its fields directly:
| Field | Type | Description |
|---|---|---|
ColorBlendFunction | BlendFunction | Equation used to combine source and destination colour channels |
ColorSourceBlend | Blend | Source colour multiplier |
ColorDestinationBlend | Blend | Destination colour multiplier |
AlphaBlendFunction | BlendFunction | Equation used to combine source and destination alpha channels |
AlphaSourceBlend | Blend | Source alpha multiplier |
AlphaDestinationBlend | Blend | Destination alpha multiplier |
BlendFactor | Color | Constant colour used when Blend::BlendFactor is selected |
ColorWriteChannels | ColorWriteChannels | Bitmask of RGBA channels that are written to the render target |
// Custom multiply blend
BlendState multiply;
multiply.ColorBlendFunction = BlendFunction::Add;
multiply.ColorSourceBlend = Blend::DestinationColor;
multiply.ColorDestinationBlend = Blend::Zero;
multiply.AlphaBlendFunction = BlendFunction::Add;
multiply.AlphaSourceBlend = Blend::One;
multiply.AlphaDestinationBlend = Blend::Zero;
graphicsDevice.BlendState = multiply;
DepthStencilState
DepthStencilState controls whether incoming fragments are tested against the depth buffer and whether they write their depth value back. It also governs stencil operations for masking and outlining effects.
Built-in presets
| Preset | Use case |
|---|---|
DepthStencilState::Default |
Depth test enabled, depth write enabled. The correct setting for opaque 3D geometry — closer geometry overwrites farther geometry. |
DepthStencilState::DepthRead |
Depth test enabled, depth write disabled. Used for transparent 3D objects: they are occluded by opaque geometry but do not themselves occlude each other or update the depth buffer. |
DepthStencilState::None |
No depth test and no depth write. The correct setting for 2D UI and SpriteBatch rendering where painter’s order (draw order) determines visibility. |
Setting DepthStencilState
// Standard 3D opaque pass
graphicsDevice.DepthStencilState = DepthStencilState::Default;
// Transparent 3D pass (test but do not write depth)
graphicsDevice.DepthStencilState = DepthStencilState::DepthRead;
// 2D HUD / SpriteBatch pass
graphicsDevice.DepthStencilState = DepthStencilState::None;
Custom DepthStencilState
| Field | Type | Description |
|---|---|---|
DepthBufferEnable | bool | Enables the depth test |
DepthBufferWriteEnable | bool | Enables writing the depth value to the depth buffer on pass |
DepthBufferFunction | CompareFunction | Comparison used for the depth test (default: Less) |
StencilEnable | bool | Enables stencil testing |
StencilFunction | CompareFunction | Comparison used for the stencil test |
StencilPass | StencilOperation | Operation applied when both stencil and depth tests pass |
StencilFail | StencilOperation | Operation applied when the stencil test fails |
ReferenceStencil | int | Reference value compared against the stencil buffer |
Stencil is not universal. Real stencil pipelines exist on Vulkan (a real stencil PSO) and SDL_GPU. DIRECTX12 has no stencil support and WebGPU's stencil is non-functional, so stencil-based masking and outlining silently do nothing on those two renderers.
RasterizerState
RasterizerState controls how triangles are converted to fragments. The most commonly adjusted settings are face culling and fill mode.
Built-in presets
| Preset | Use case |
|---|---|
RasterizerState::CullCounterClockwise |
Default. Back faces (counter-clockwise winding in screen space) are culled. Use this for all standard 3D geometry with correctly wound vertices. |
RasterizerState::CullClockwise |
Cull back faces where clockwise winding defines the front face. Use when importing geometry from tools that use a different handedness convention. |
RasterizerState::CullNone |
No face culling — both sides of every triangle are rasterised. Use for flat double-sided geometry such as leaves, cloth, or decals. |
What each cull mode does to one triangle
The three frames below come from CNA's XNA oracle corpus (tools/xna-oracle/) — they are the real Microsoft XNA 4.0 runtime's own output, which CNA's DIRECTX9 renderer is diffed against at --tolerance 0. A single clockwise-wound triangle is drawn under each CullMode. See the oracle corpus for details.
CullMode::None — both faces rasterise, so the triangle is visible. Real XNA 4.0 output; CNA matches it pixel-for-pixel.
CullClockwiseFace — the triangle's winding marks it as back-facing, so it disappears entirely. Real XNA 4.0 output; CNA matches it pixel-for-pixel.
CullCounterClockwiseFace — the default; this triangle is front-facing and survives. Real XNA 4.0 output; CNA matches it pixel-for-pixel.
Setting RasterizerState
// Default for 3D (already the default on GraphicsDevice)
graphicsDevice.RasterizerState = RasterizerState::CullCounterClockwise;
// Double-sided geometry
graphicsDevice.RasterizerState = RasterizerState::CullNone;
Custom RasterizerState
| Field | Type | Description |
|---|---|---|
CullMode | CullMode | Which faces are culled: None, CullClockwiseFace, or CullCounterClockwiseFace |
FillMode | FillMode | Solid (default) or WireFrame — see note below |
MultiSampleAntiAlias | bool | Enables MSAA rasterisation when the render target is multisampled |
ScissorTestEnable | bool | Clips rasterisation to graphicsDevice.ScissorRectangle |
DepthBias | float | Constant depth offset added to each fragment — used to prevent z-fighting on coplanar surfaces |
SlopeScaleDepthBias | float | Depth bias scaled by the polygon slope — useful for shadow mapping |
WireFrame is renderer-dependent. FillMode::WireFrame is not honoured everywhere, and a renderer that ignores it will silently keep drawing solid rather than raising an error. Because CNA picks its renderer at build time, verify wireframe visually against the renderer you actually ship. Note also that on bgfx, draw offsets (startIndex/baseVertex) are ignored outside the wireframe path.
SamplerState
SamplerState describes how textures are sampled — the filtering algorithm applied when a texture is magnified or minified, and how texture coordinates outside the [0, 1] range are handled. Up to 16 independent sampler slots are available, addressed via graphicsDevice.SamplerStates[n].
Built-in presets
| Preset | Use case |
|---|---|
SamplerState::LinearClamp |
Bilinear filtering with clamping at texture edges. The most common preset for UI textures, skyboxes, and any texture that should not tile. |
SamplerState::LinearWrap |
Bilinear filtering with tiling/repeating. Use for repeating terrain textures, tiled floors, and other tileable surfaces. |
SamplerState::PointClamp |
Nearest-neighbour (point) filtering with clamping. Essential for pixel art, retro-style games, and any content where bilinear blurring is undesirable. |
SamplerState::PointWrap |
Nearest-neighbour filtering with tiling. Use for pixel-art tilesets and sprite sheets that need to repeat. |
SamplerState::AnisotropicClamp |
Anisotropic filtering with clamping. Preserves texture sharpness on surfaces viewed at a glancing angle (floors, roads). Higher quality than bilinear at the cost of a small GPU overhead. |
SamplerState::AnisotropicWrap |
Anisotropic filtering with tiling. Best quality for repeating terrain and surface textures seen at oblique angles. |
Setting SamplerState
// Set slot 0 for the primary texture
graphicsDevice.SamplerStates[0] = SamplerState::LinearClamp;
// Set slot 1 for a secondary (e.g. lightmap) texture
graphicsDevice.SamplerStates[1] = SamplerState::LinearWrap;
Custom SamplerState
| Field | Type | Description |
|---|---|---|
Filter | TextureFilter | Filtering algorithm: Linear, Point, Anisotropic, and mip-level variants |
AddressU | TextureAddressMode | Behaviour outside [0,1] on the U axis: Wrap, Clamp, Mirror, or Border |
AddressV | TextureAddressMode | Behaviour outside [0,1] on the V axis |
AddressW | TextureAddressMode | Behaviour outside [0,1] on the W axis (volume textures) |
MaxAnisotropy | int | Maximum anisotropy level (1–16) used when Filter is Anisotropic |
MipMapLevelOfDetailBias | float | Biases mip-map selection; negative values select sharper (higher-resolution) mips |
MaxMipLevel | int | Clamps mip-map selection to this level or coarser (0 = no limit) |
Wrap versus Mirror addressing
The difference between the two repeating address modes is easiest to see on a two-colour texture sampled past U = 1. Both frames are genuine Microsoft XNA 4.0 runtime output from CNA's oracle corpus (tools/xna-oracle/), diffed against CNA's DIRECTX9 renderer at --tolerance 0.
TextureAddressMode::Wrap — the texture restarts at every integer U, so the red-then-green pattern simply repeats. Real XNA 4.0 output; CNA matches it pixel-for-pixel.
TextureAddressMode::Mirror — every other repeat is flipped, so the two green halves meet and form one continuous block. Real XNA 4.0 output; CNA matches it pixel-for-pixel.
Viewport and ScissorRectangle
Two additional device properties control which region of the render target receives output.
Viewport
graphicsDevice.Viewport is a Viewport struct that defines the sub-rectangle of the render target used for rendering and the depth range:
// Full-screen viewport with default depth range
graphicsDevice.Viewport = Viewport(0, 0, screenWidth, screenHeight, 0.0f, 1.0f);
// Split-screen: render to the left half
graphicsDevice.Viewport = Viewport(0, 0, screenWidth / 2, screenHeight, 0.0f, 1.0f);
The constructor parameters are (x, y, width, height, minDepth, maxDepth). minDepth and maxDepth default to 0.0f and 1.0f and should rarely need to change.
ScissorRectangle
graphicsDevice.ScissorRectangle is a Rectangle that clips rasterisation to a sub-region. It only takes effect when RasterizerState.ScissorTestEnable is true:
// Enable scissor test in the rasterizer state
RasterizerState scissorRS;
scissorRS.CullMode = CullMode::CullCounterClockwiseFace;
scissorRS.ScissorTestEnable = true;
graphicsDevice.RasterizerState = scissorRS;
// Define the scissor region
graphicsDevice.ScissorRectangle = Rectangle(100, 100, 400, 300);
// Only pixels inside (100,100)-(500,400) are written
graphicsDevice.DrawPrimitives(PrimitiveType::TriangleList, 0, triangleCount);
// Disable scissor test afterwards
graphicsDevice.RasterizerState = RasterizerState::CullCounterClockwise;
DIRECTX12 implements neither scissor nor viewport. On a DIRECTX12 build, setting ScissorRectangle or Viewport has no effect and rasterisation is not clipped — split-screen and clipped-UI techniques will render full-screen instead. DIRECTX12 also has no blend factor, so Blend::BlendFactor and BlendState::BlendFactor do nothing there.
Common patterns
Pattern 1: 3D scene with 2D HUD
The most common pattern in any game: render the 3D world, then overlay a 2D HUD using SpriteBatch. State must be switched explicitly between the two passes.
// --- 3D world pass ---
graphicsDevice.BlendState = BlendState::Opaque;
graphicsDevice.DepthStencilState = DepthStencilState::Default;
graphicsDevice.RasterizerState = RasterizerState::CullCounterClockwise;
graphicsDevice.SamplerStates[0] = SamplerState::LinearWrap;
DrawScene(graphicsDevice);
// --- 2D HUD pass ---
// SpriteBatch sets its own blend and depth state internally,
// but setting them explicitly before Begin() is good practice.
graphicsDevice.BlendState = BlendState::AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState::None;
graphicsDevice.RasterizerState = RasterizerState::CullNone;
graphicsDevice.SamplerStates[0] = SamplerState::LinearClamp;
spriteBatch.Begin();
DrawHUD(spriteBatch);
spriteBatch.End();
Pattern 2: Opaque then transparent 3D objects
Opaque objects should always be drawn before transparent objects. After the opaque pass, switch to DepthRead and sort transparent objects back-to-front before drawing.
// --- Opaque pass ---
graphicsDevice.BlendState = BlendState::Opaque;
graphicsDevice.DepthStencilState = DepthStencilState::Default;
for (auto& mesh : opaqueMeshes)
mesh.Draw(graphicsDevice, effect);
// --- Transparent pass ---
// Sort transparent objects back-to-front from the camera
std::sort(transparentMeshes.begin(), transparentMeshes.end(),
[&](const auto& a, const auto& b) {
return a.DistanceTo(camera) > b.DistanceTo(camera);
});
graphicsDevice.BlendState = BlendState::AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState::DepthRead;
for (auto& mesh : transparentMeshes)
mesh.Draw(graphicsDevice, effect);
Pattern 3: Additive particle system
Particle effects such as fire, sparks, and laser bolts accumulate light additively. Depth writing must be disabled so particles do not occlude each other based on draw order.
graphicsDevice.BlendState = BlendState::Additive;
graphicsDevice.DepthStencilState = DepthStencilState::DepthRead;
graphicsDevice.RasterizerState = RasterizerState::CullNone;
graphicsDevice.SamplerStates[0] = SamplerState::LinearClamp;
particleSystem.Draw(graphicsDevice, camera);
// Reset state for subsequent passes
graphicsDevice.BlendState = BlendState::Opaque;
graphicsDevice.DepthStencilState = DepthStencilState::Default;
Code examples
Example 1: Setting BlendState for HUD sprites
// Non-premultiplied PNG loaded from disk (straight alpha)
graphicsDevice.BlendState = BlendState::NonPremultiplied;
spriteBatch.Begin(SpriteSortMode::Deferred,
BlendState::NonPremultiplied);
spriteBatch.Draw(hudTexture, Vector2(10.0f, 10.0f), Color::White);
spriteBatch.End();
// Return to premultiplied alpha for the next SpriteBatch call
graphicsDevice.BlendState = BlendState::AlphaBlend;
Example 2: 3D opaque and transparent pass setup
void Game::Draw(GameTime gameTime)
{
graphicsDevice.Clear(Color::CornflowerBlue);
// Opaque geometry
graphicsDevice.BlendState = BlendState::Opaque;
graphicsDevice.DepthStencilState = DepthStencilState::Default;
graphicsDevice.RasterizerState = RasterizerState::CullCounterClockwise;
DrawOpaqueMeshes();
// Transparent geometry (test depth, do not write)
graphicsDevice.BlendState = BlendState::AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState::DepthRead;
DrawTransparentMeshes();
// 2D overlay
graphicsDevice.DepthStencilState = DepthStencilState::None;
spriteBatch.Begin();
DrawHUD();
spriteBatch.End();
}
Example 3: Additive particle blending
// Particle system update + draw
void ParticleSystem::Draw(GraphicsDevice& gd, const Camera& cam)
{
gd.BlendState = BlendState::Additive;
gd.DepthStencilState = DepthStencilState::DepthRead;
gd.RasterizerState = RasterizerState::CullNone;
gd.SamplerStates[0] = SamplerState::LinearClamp;
particleEffect->SetView(cam.View());
particleEffect->SetProjection(cam.Projection());
for (auto& batch : particleBatches) {
particleEffect->SetWorld(batch.Transform);
particleEffect->SetTexture(batch.Texture);
particleEffect->CurrentTechnique->Passes[0]->Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList,
batch.VertexOffset, batch.PrimitiveCount);
}
}
Example 4: Custom RasterizerState (no culling + wireframe)
// Build a custom rasterizer state for debug wireframe rendering.
// WireFrame is not honoured by every renderer — verify it visually
// against the renderer this build was configured with.
RasterizerState wireframeState;
wireframeState.CullMode = CullMode::None;
wireframeState.FillMode = FillMode::WireFrame;
wireframeState.MultiSampleAntiAlias = false;
graphicsDevice.RasterizerState = wireframeState;
DrawSceneWireframe();
graphicsDevice.RasterizerState = RasterizerState::CullCounterClockwise;
Example 5: Per-slot sampler state for multi-texture
// DualTextureEffect uses two texture slots.
// Slot 0: diffuse texture with linear wrap (tiling surface)
// Slot 1: lightmap with linear clamp (unique per-object UV)
graphicsDevice.SamplerStates[0] = SamplerState::LinearWrap;
graphicsDevice.SamplerStates[1] = SamplerState::LinearClamp;
auto effect = std::make_shared<DualTextureEffect>(graphicsDevice);
effect->SetWorld(Matrix::Identity);
effect->SetView(camera.View());
effect->SetProjection(camera.Projection());
effect->SetTexture(diffuseTexture);
effect->SetTexture2(lightmapTexture);
effect->CurrentTechnique->Passes[0]->Apply();
graphicsDevice.DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, indexCount / 3);