Tutorial 101: Querying Renderer Capabilities
What you’ll learn
- What the 19
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, the detailed
RendererCapabilityProfile(32 features, 22 limits, per-format usage), and the policy switch that decides what happens when you call anyway.
Before you start — Tutorial 72: Choosing a Renderer introduces CNA's 14 renderer identities, the compact single-renderer default and the opt-in multi-renderer build.
CNA's default build compiles one renderer, while an opt-in multi-renderer build can contain several and select one before device creation. The 14 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 most misread API in CNA, because for the original fourteen members its default answer is yes. The five newest members were added the other way round — they fail closed — and this tutorial is about telling the two kinds apart and reading both correctly.
The 19 capabilities
CNA::GraphicsCapability is a closed enum of 19 members: the 14 that existed in alpha.1 and, in this snapshot, five newer ones for HDR, compute and GPU-driven rendering. Every one of them maps to a real gap that exists somewhere in the renderer set — it is not a speculative wish list. Numeric values are append-only, so the newer members sit at the end.
| 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(). |
CompiledEffects | XNA/FNA Direct3D 9 Effect Framework binaries can be reflected and executed, including their passes and state. |
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. |
FloatRenderTargets | New in this snapshot. A RenderTarget2D created with SurfaceFormat::Single, Vector2 or Vector4 really stores unclamped 32-bit float values, instead of the 8-bit Color target a renderer would otherwise substitute. |
HalfFloatRenderTargets | New. The 16-bit equivalent — HalfSingle, HalfVector2, HalfVector4 and HdrBlendable. The practical HDR format, available on far more hardware than the 32-bit one. |
HalfFloatTextureLinearFiltering | New. Linear (and mip) filtering when sampling a half-float texture, as opposed to merely rendering into one. A context can render to RGBA16F and still sample it only with nearest filtering. |
ComputeShaders | New. The renderer can run compute shaders together with the storage-buffer path they read and write. This is a statement about the renderer: CNA has no public class through which game code dispatches compute work. |
IndirectDraw | New. Indirect draws: counts and offsets are read out of a GPU buffer instead of being passed as arguments, so a compute pass can decide how much to draw without a CPU round trip. GraphicsDevice declares the indirect draw calls, but no public class creates the argument buffer they need. |
Note the deliberately narrow wording on two of them. Texture3D describes storage and readback only — SetData/GetData persist; whether a shader can sample the volume is a separate answer in the detailed profile (RendererFeature::Texture3DSampling). 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 is explicit for five entries and permissive for the rest. MultiStreamVertexInput returns false, CompiledEffects calls the false-by-default SupportsCompiledEffects() opt-in, StencilBuffer forwards to SupportsStencilBuffer(), and the two float-render-target entries return false. Everything else falls through to 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. Since alpha.1 almost all of them do: DIRECTX9 is the only renderer that has no SupportsCapability() override at all. DIRECTX11 and SDL_GPU, which inherited the default in alpha.1, now author their own answers.
| Renderer | What it reports today |
|---|---|
DIRECTX9 | The shared default, unmodified: false-by-default for multi-stream input and compiled effects, the renderer's depth/stencil answer for stencil, and permissive true answers for the remaining original entries. Its render-target count is bounded by the XNA profile (Reach one, HiDef four). |
DIRECTX11 | An exhaustive switch with no default arm. MSAA is a real multisample-count query, the float and half-float entries come from DXGI format-support queries, and ComputeShaders and IndirectDraw answer true on a device at feature level 11_0 or above (an answer about the renderer; CNA has no public class that dispatches compute work). |
SDL_GPU | Its own switch: MSAA and stencil are probed from the created device and formats, OcclusionQuery is false, and CustomEffects is true only where the optional libshaderc route is built. Ask the GraphicsDevice, not the renderer interface — the device derives compute, indirect draw and the float-target entries itself. |
Being inherited does not make a renderer weak — DIRECTX9 is the renderer recorded in the repository as matching all 39 scenes of CNA's XNA oracle corpus exactly, under Wine + DXVK. But its capability answers are inherited, not authored, so they describe the shape of the contract rather than proof that a particular feature has been exercised. Probe the ones you depend on.
Six answers are derived, and fail closed
The call you actually make, GraphicsDevice::SupportsCapability(), does not simply forward to the renderer’s switch. Many renderer switches end in default: return true, which would have every renderer claim a capability it has never heard of, so six of the newer answers are taken from separate, false-by-default renderer queries instead:
| Capability | Where the device-level answer comes from |
|---|---|
CompiledEffects | The renderer’s SupportsCompiledEffects() (false unless the renderer opted in, and for most of them the CMake option is off by default). |
FloatRenderTargets | Whether an RGBA32F (SurfaceFormat::Vector4) render target can really be created. |
HalfFloatRenderTargets | Whether an HdrBlendable render target can really be created. |
HalfFloatTextureLinearFiltering | The renderer’s half-float filtering query (false by default). |
ComputeShaders | The renderer’s compute query (false by default). |
IndirectDraw | The renderer’s indirect-draw query (false by default). |
MultipleRenderTargets is also combined with the XNA profile: the renderer’s answer is ANDed with the profile’s render-target limit, so a Reach-profile device reports false even on a renderer that could do more.
A false is a promise; a true is only an absence of one
This is the whole lesson, and it still holds. For the original fourteen members true is what you get for free, so 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 — with the exception of the six derived entries above, where atruehas had to be earned.
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. |
METAL | Thirteen named switch arms with no permissive fallback: twelve answer true and MultiSampleAntiAliasing is answered per device (true wherever the GPU multisamples). The six newer members reach the switch’s final false, but the device derives them: compiled effects are true only with CNA_METAL_COMPILED_EFFECTS=ON, the float and half-float render-target answers are true under HiDef, half-float linear filtering is true, and compute and indirect draws are false. |
SDL_RENDERER | 2D-only: false for everything except AdditiveBlending, where it has an explicit, tested additive path. |
FNA3D | An authored false for CustomEffects (it only accepts compiled effects), CompiledEffects always on, and real device queries for instancing and MSAA. See Tutorial 108. |
| The GL family | Profile-dependent, and partly a live driver query. See Tutorial 102. |
HEADLESS | Reports ThreeD as true and renders nothing at all; Texture3D, multi-stream input and additive blending are false. A capability is not a pixel. |
The detailed profile: features, limits and formats
Nineteen yes/no summaries cannot say everything. ComputeShaders does not tell you how big a work group may be; FloatRenderTargets does not say whether the target can also be blended. This snapshot therefore adds a second, finer API, CNA::RendererCapabilityProfile, that separates what the summary lumps together:
- 32
RendererFeatureids. The legacy summaries split further — for exampleShaderEffectsversusShaderEffectSourceExecution,Texture3DStorageversusTexture3DSampling— plusComputeImageBinding,ShadowSampling,ImageBasedLighting,GpuTimers,BaseInstanceDrawingand six shader-dialect flags (desktop GLSL, ES GLSL, Vulkan GLSL, HLSL, MSL, WGSL). - A four-state answer per feature:
Unknown(never audited or probed),Unsupported,Supported, orRestricted(a described subset), each with an optional English note. - 22
RendererLimitids — maximum texture dimension, vertex streams, compute work-group counts and sizes, buffer byte ranges, attachment counts, offset alignments and the GPU timestamp period — each returned as a value plus aknownflag. - Per-format usage for all 27
SurfaceFormatvalues: texture storage, sampling, filtering, render target, blending, storage-image access, transfers, mip-mapping and multisampling, as separate known and supported masks. - A generated English report of all of the above.
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "CNA/RendererCapabilityProfile.hpp"
using Microsoft::Xna::Framework::Graphics::GraphicsDevice;
using Microsoft::Xna::Framework::Graphics::SurfaceFormat;
using CNA::RendererFeature;
using CNA::RendererFeatureSupport;
using CNA::RendererFormatUsage;
using CNA::RendererLimit;
bool CanUseHdrTargets(const GraphicsDevice& device)
{
// 1. A boolean shortcut: true only for an unqualified "Supported".
if (device.SupportsRendererFeatureEXT(RendererFeature::Float16RenderTargets))
return true;
// 2. The full four-state answer. "Restricted" means a described subset, and the
// renderer's own qualification text says which one.
const CNA::RendererCapabilityProfile& profile = device.GetRendererCapabilityProfileEXT();
const CNA::RendererFeatureInfo& f16 = profile.GetFeature(RendererFeature::Float16RenderTargets);
if (f16.support != RendererFeatureSupport::Restricted)
return false; // Unknown or Unsupported
// 3. Restricted: ask about the exact format and usage you need. "Classified"
// and "supported" are separate masks.
const CNA::RendererFormatSupport hdr =
device.GetRendererSurfaceFormatSupportEXT(SurfaceFormat::HdrBlendable);
return hdr.Supports(RendererFormatUsage::RenderTarget) &&
hdr.Supports(RendererFormatUsage::Blendable);
}
std::uint64_t MaxTextureSize(const GraphicsDevice& device)
{
const CNA::RendererLimitValue limit =
device.GetRendererLimitEXT(RendererLimit::MaxTextureDimension);
return limit.known ? limit.value : 0; // "unknown" is not "zero"
}
The profile is built lazily from the renderer’s own canonical queries and cached on the GraphicsDevice; it is rebuilt when the native renderer is reconstructed or a capability-affecting presentation setting changes, so copy it if you need to keep a snapshot across a device reset. Because every entry starts as Unknown and only becomes Supported through an explicit mapping, an Unknown is the honest answer for a renderer that has not been audited — treat it like a true you have not verified, not like a false. GetRendererCapabilityReportEXT() returns the whole thing as text, which is handy in a bug report. The same data is exposed through five C functions. Tutorial 133 walks through the API end to end.
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 THIS device really runs on (a runtime answer, which matters in
// a multi-renderer build).
std::string_view name = device.GetGraphicsRendererName(); // e.g. "OPENGLES3"
CNA::GraphicsRendererType type = device.GetGraphicsRendererType();
// 2. The renderer this build was configured with -- 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. The two device methods answer for the renderer the device actually resolved to, so they are no longer constexpr: in a multi-renderer build a compile-time answer could only describe the build default. CNA::getCurrentGraphicsRendererType() is still a constant expression, and still reports only that default. 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 (a std::runtime_error of the form “<renderer> does not support 3D: <method>”). 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.
The 2D-only SDL_RENDERER routes through the shared handler and honours WarnAndStub. (STUB and HEADLESS never reach that path at all — the first draws nothing and the second only traces.) The same switch exists in the C API as cna_graphics_device_set_unsupported_3d_call_behavior.
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, the one renderer that reports the inherited default for every original member, and on any renderer that answers with adefault: return truearm. - Ask the
GraphicsDevice, not the renderer interface: the six derived answers only exist at device level. - When a number matters (texture size, work-group size, attachment count), read the
RendererLimitand check itsknownflag instead of guessing from a capability. - 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 — three profiles whose capability answers depend on the live context
- Tutorial 133: The renderer capability profile — features, limits and formats in detail
- Tutorial 107: CPU-only renderers — reading pixels back without a GPU
- Tutorial 72: Choosing a Renderer
- Renderers reference
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Capability answers, interface defaults and draw evidence — What a GraphicsCapability answer asks and guarantees in CNA: the 19 contract questions, renderer versus device polarity, known report mismatches, the unsupported-3D policy, four interface-default failure shapes and portable draw claims.
- Evidence tiers of the native modern GPU renderers — What VULKAN, SDL_GPU, WEBGPU and METAL implement at this snapshot, what evidence backs each, why a capability bit is not evidence, and the defect shapes these renderers exposed.
- The renderer contract: IGraphicsRenderer defaults, factories and failure shapes — Which IGraphicsRenderer bodies a renderer family must write, what each inherited default does to a public call, how null factories fail, and the evidence ladder behind a feature.