Tutorial 101: Querying Renderer Capabilities
What you’ll learn
- What the 13
GraphicsCapabilitymembers promise, and how to query one. - Why
SupportsCapability()fails open, and why afalseis worth far more than atrue. - How to write a real probe for the one feature your game actually depends on.
- The queries that are reliable, and the policy switch that decides what happens when you call anyway.
Before you start — Tutorial 72: Choosing a Renderer introduces CNA's 46 renderer identities and the fact that exactly one is compiled into any build.
CNA compiles exactly one renderer into a build, and those renderers differ enormously: some have no 3D pipeline, some have no shader stage, some never produce a pixel. GraphicsDevice::SupportsCapability() exists so you can branch before reaching for a feature instead of catching an exception afterwards.
It is a genuinely useful tool. It is also the single most misread API in CNA, because its default answer is yes. This tutorial is about reading it correctly.
The 13 capabilities
CNA::GraphicsCapability is a closed enum of 13 members. Every one of them maps to a real gap that exists somewhere in the renderer set — it is not a speculative wish list.
| Member | What a true is supposed to mean |
|---|---|
ThreeD | The 3D pipeline as a whole: vertex/index buffers, 3D draw calls, depth/stencil clears and state. |
DepthStencilBuffer | A complete, real depth/stencil attachment on the active target. |
StencilBuffer | A stencil plane usable independently of depth. Separate so a 2D renderer can advertise a stencil-mask extension without claiming a depth attachment. |
MultiSampleAntiAliasing | Any sample count above 1. |
MultipleRenderTargets | More than one simultaneous render target. |
AnisotropicFiltering | Anisotropic texture filtering. Device- and driver-dependent on several renderers. |
WireFrame | RasterizerState.FillMode = FillMode::WireFrame. |
OcclusionQuery | Real GPU occlusion queries — Begin/End/PixelCount. |
CustomEffects | A non-stock Effect passed to SpriteBatch::Begin(). |
Texture3D | Real volume-texture storage — SetData/GetData genuinely persist. It never promises shader sampling. |
MultiStreamVertexInput | More than one VertexBufferBinding of the same input rate on one draw. |
Instancing | Hardware instancing via DrawInstancedPrimitives. |
AdditiveBlending | BlendState::Additive uses a documented additive colour path rather than silently degrading to ordinary alpha blending. |
Note the deliberately narrow wording on two of them. Texture3D describes storage and readback only — Skia reports it true for bounded CPU transfer storage while keeping ThreeD and CustomEffects false. And AdditiveBlending is about the fidelity of this renderer's implementation, not about whether the underlying API could theoretically express additive blending.
Querying a capability
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "CNA/GraphicsCapability.hpp"
using CNA::GraphicsCapability;
void MyGame::Draw(const GameTime&)
{
auto& device = getGraphicsDeviceProperty();
if (device.SupportsCapability(GraphicsCapability::MultipleRenderTargets))
DrawDeferredPath();
else
DrawForwardPath();
}
SupportsCapability() is a CNA extension, not part of the XNA 4.0 surface. It is tagged CNAEXT, so a translation unit compiled with CNA_STRICT_XNA_API plus -Werror=deprecated-declarations will refuse to build if it calls this method. If you are keeping a module XNA-pure, keep the capability checks out of it.
The default that fails open
The shared base implementation returns true for every capability except two. MultiStreamVertexInput defaults to false, and StencilBuffer is forwarded to the renderer's own SupportsStencilBuffer(). Everything else falls through to a flat return true; — whether or not the renderer implements it.
The comment on that default is explicit that every renderer with a narrower contract must override the applicable entries truthfully. Most do. Four do not override SupportsCapability() at all:
| Renderer | What it reports today |
|---|---|
DIRECTX9 | The shared default, unmodified: true for eleven members, false for MultiStreamVertexInput, and the renderer's depth/stencil answer for StencilBuffer. |
DIRECTX11 | |
DIRECTX12 | |
SDL_GPU |
These are not weak renderers — DIRECTX9 is the only one that matches all 39 scenes of CNA's XNA oracle corpus exactly. But their capability answers are inherited, not authored. Ask DIRECTX9 whether it supports occlusion queries and it says yes; CreateOcclusionQuery() on that renderer falls through to the shared default that returns nullptr.
A false is a promise; a true is only an absence of one
This is the whole lesson. Because true is what you get for free, a renderer only ever reports false by making a deliberate, explicit decision to say no. That makes the two answers mean very different things:
falseis authored. Somebody looked at that capability on that renderer and wrote the negative. Trust it, and take the fallback path.truemeans “not contradicted”. It may be an authored yes backed by a real implementation, or it may be the default nobody has revisited. The API cannot tell you which.
Some renderers do report the whole set explicitly, and their answers carry real information:
| Renderer | How it answers |
|---|---|
STUB | false for every capability — the only renderer that does. It renders nothing and says so. |
OPENVG | false for all 13, each with its own recorded reason. |
METAL | An explicit answer for all 13, with no permissive default. Six are false. |
SKIA | An exhaustive switch with no default arm, so a capability added later is a compiler diagnostic rather than a silently wrong answer. Only Texture3D and AdditiveBlending are true. |
FNA3D | Authored falses for CustomEffects and Instancing, a real device query for MSAA. See Tutorial 108. |
| The GL family | Profile-dependent, and partly a live driver query. See Tutorial 102. |
HTML_DOM, SVG_DOM | false for everything except AdditiveBlending, which is answered by asking the running browser whether it supports mix-blend-mode: plus-lighter. See Tutorial 105. |
HEADLESS | Reports ThreeD as true and renders nothing at all. A capability is not a pixel. |
Writing a real probe
When a true is load-bearing — your whole lighting path depends on MRT, or your UI depends on additive blending — do not stop at the query. Perform the operation once, during startup, and record what happened.
#include "Microsoft/Xna/Framework/Graphics/RenderTarget2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/RenderTargetBinding.hpp"
// Run once, after the GraphicsDevice exists. Cheap: two 4x4 targets.
bool ProbeMultipleRenderTargets(GraphicsDevice& device)
{
// A false here is authored and final -- believe it and stop.
if (!device.SupportsCapability(GraphicsCapability::MultipleRenderTargets))
return false;
try
{
RenderTarget2D a(device, 4, 4);
RenderTarget2D b(device, 4, 4);
std::vector<RenderTargetBinding> set;
set.emplace_back(&a);
set.emplace_back(&b);
device.SetRenderTargets(set);
device.SetRenderTarget(nullptr);
return true;
}
catch (const std::exception&)
{
return false; // the "yes" was inherited, not implemented
}
}
Three things make this pattern work. It short-circuits on the trustworthy answer, so it costs nothing on a renderer that already said no. It exercises the real API rather than a proxy. And it runs once at startup, so the result can be cached in a struct your render code reads every frame.
For capabilities that produce a wrong image rather than an exception — additive blending degrading to alpha blending is the classic case — a probe needs pixels: draw into a small RenderTarget2D, read it back with GetBackBufferData(), and compare. Tutorial 107 covers that readback pattern.
One capability the shared layer enforces for you. GraphicsDevice checks MultiStreamVertexInput itself before native submission: bind a VertexDeclaration split across several buffers, or several per-instance streams, on a renderer that reports false, and you get a System::NotSupportedException naming the stream counts rather than a draw that silently renders from stream 0 alone. The classic shapes — one per-vertex stream, or one per-vertex plus one per-instance stream — need no capability at all and are never affected.
Three queries you can rely on
Not everything on this part of the API is soft. These three are exact:
#include "CNA/GraphicsRendererType.hpp"
// 1. Which renderer is compiled in -- a compile-time constant.
std::string_view name = device.GetGraphicsRendererName(); // e.g. "OPENGLES3"
// 2. The same thing as an enum, usable in a constant expression.
static_assert(CNA::getCurrentGraphicsRendererType() != CNA::GraphicsRendererType::Stub,
"This target needs a renderer that draws something.");
// 3. The renderer's real maximum single-axis texture dimension.
const int maxDim = device.GetMaxTextureDimension();
The name matches the CNA_GRAPHICS_RENDERER CMake value exactly, and both it and the type are resolved from the compile definition CMake sets, so they are constexpr and cannot drift. GetMaxTextureDimension() answers from the renderer rather than from a hardcoded profile number — query it before creating or accepting a texture whose size comes from a file or a user.
Changing what happens when you call anyway
Capability queries describe what a renderer can do. A separate policy decides what happens if your code calls an unsupported 3D operation regardless:
#include "CNA/Unsupported3DGraphicsCallBehavior.hpp"
device.SetUnsupported3DGraphicsCallBehavior(
CNA::Unsupported3DGraphicsCallBehavior::WarnAndStub);
Throw is the default and preserves each renderer's established exceptions and null results. WarnAndStub logs each unsupported operation once and substitutes a safe no-op or a null-object resource, so a 3D game boots and runs on a 2D-only renderer instead of aborting on its first draw call. The policy is device-local, takes effect immediately, and clears the warn-once history so the next stubbed operation is visible in the log.
WarnAndStub does not change SupportsCapability() results, and it does not suppress argument errors, lifetime errors, driver failures, or implementation bugs on an otherwise 3D-capable renderer. It only softens the permanent 2D boundary.
A short checklist
- Treat
falseas final andtrueas unverified. - Probe once at startup for anything load-bearing, and cache the answer.
- Be especially careful on
DIRECTX9,DIRECTX11,DIRECTX12andSDL_GPU, which report the inherited default for every member. - Remember that a capability describes an implementation, not an image:
HEADLESSanswerstruetoThreeDand draws nothing. - Keep
SupportsCapability()out of any translation unit you compile withCNA_STRICT_XNA_API.
Where to go next
- Tutorial 102: The OpenGL family — five profiles whose capability answers genuinely differ
- Tutorial 107: CPU-only renderers — reading pixels back without a GPU
- Tutorial 72: Choosing a Renderer
- Renderers reference