Tutorial 133: Read the Renderer Capability Profile
What you’ll learn: how the renderer capability profile relates to SupportsCapability(), how to read features, limits, per-format usage and the English report from a GraphicsDevice, how to make render-path decisions from them, and how the same data appears in the C API.
Before you start — Tutorial 101: Querying renderer capabilities, which introduces GraphicsDevice::SupportsCapability() and its 17 GraphicsCapability members. This tutorial documents CNA snapshot 3b43bd86 (branch next); the profile API does not exist in the alpha.1 tag.
SupportsCapability() answers a yes/no question per GraphicsCapability member. That is enough to choose between a deferred and a forward path, but it cannot say how many colour attachments a draw may write, whether HalfVector4 can also be blended into, whether a renderer merely accepts a ShaderEffect or actually executes your source, or whether the answer is genuinely “no” or just “nobody classified this”. This snapshot adds a second, richer query surface for exactly that: the renderer capability profile.
It is an immutable snapshot of one device's answers, with four parts: 33 features (each with a four-state answer and an optional English note), 8 numeric limits (each with an explicit known/unknown flag), per-format usage for all 27 SurfaceFormat values (separate known and supported masks), and a generated English report. In this tutorial you will read each part, write a small program that dumps a device's profile, use the answers to make two real decisions, and read the same data through the experimental C API.
These are CNA extensions, not XNA 4.0. Every device-level accessor below is tagged CNAEXT, exactly like SupportsCapability(). A translation unit compiled with CNA_STRICT_XNA_API and -Werror=deprecated-declarations will refuse to build if it calls one. The profile types themselves live in namespace CNA, in CNA/RendererCapabilityProfile.hpp.
How the profile relates to SupportsCapability()
Seventeen of the 33 features are the 17 GraphicsCapability members seen through the new API. The profile builds each of them by calling GraphicsDevice::SupportsCapability(), so the two can never disagree about those seventeen — including the device-level derivations Tutorial 101 describes (the float-target answers come from render-target format support, half-float filtering and compiled effects from dedicated renderer queries, and MultipleRenderTargets and Texture3D are combined with the XNA graphics profile: Reach allows one render target and no volume textures, HiDef four targets).
RendererFeature | Same as GraphicsCapability |
|---|---|
ThreeDimensionalPipeline | ThreeD |
DepthStencilBuffer, MultiSampleAntiAliasing, MultipleRenderTargets, AnisotropicFiltering, MultiStreamVertexInput, StencilBuffer, AdditiveBlending | the member of the same name |
WireFrameRasterization | WireFrame |
OcclusionQueries | OcclusionQuery |
ShaderEffects | CustomEffects |
Texture3DStorage | Texture3D |
InstancedDrawing | Instancing |
CompiledXnaEffects | CompiledEffects |
Float32RenderTargets, Float16RenderTargets | FloatRenderTargets, HalfFloatRenderTargets |
Float16TextureLinearFiltering | HalfFloatTextureLinearFiltering |
The other sixteen features have no GraphicsCapability member. They are the reason the profile exists: ShaderEffectSourceExecution, Texture3DSampling, ShadowSampling, ImageBasedLighting, BaseInstanceDrawing, SamplerLevelOfDetailBias, PbrEffectShading, SkinnedPbrEffectShading, MultiSampleMask, RenderTargetReadback and six shader-dialect flags (ShaderDialectGlslDesktop, ShaderDialectGlslEs, ShaderDialectGlslVulkan, ShaderDialectHlsl, ShaderDialectMsl, ShaderDialectWgsl).
Reach the profile from your game
The profile belongs to the GraphicsDevice. It is built lazily on the first request, cached, and thrown away whenever the native renderer is rebuilt (for example when a multisample-count change recreates the renderer) or the device is destroyed. That means a reference you hold can dangle after such an event; copy the profile if you need it to outlive one.
GraphicsDevice member (all const, all CNAEXT) | Returns |
|---|---|
GetRendererCapabilityProfileEXT() | const CNA::RendererCapabilityProfile& — the cached snapshot |
GetRendererFeatureSupportEXT(feature) | CNA::RendererFeatureSupport — the four-state answer |
SupportsRendererFeatureEXT(feature) | bool — true only for an unqualified Supported |
GetRendererLimitEXT(limit) | CNA::RendererLimitValue — { known, value } |
GetRendererSurfaceFormatSupportEXT(format) | CNA::RendererFormatSupport — known and supported usage masks |
GetRendererCapabilityReportEXT() | std::string_view — the English report, valid until the profile is invalidated |
The profile object itself has the matching read-only methods: GetRendererName(), GetFeature(feature) (a RendererFeatureInfo with support and note), Supports(feature), GetLimit(limit), GetSurfaceFormatSupport(ordinal), GetAdditionalLimitationsText() and GetEnglishReport(). Free functions in CNA enumerate and name the identities: AllRendererFeatures(), GetRendererFeatureName(), GetRendererFeatureDescription(), AllRendererLimits() and GetRendererLimitName(). Enumerator values are public and append-only, so the same numbers are usable through the C ABI.
Features and the four-state answer
Each feature has one of four answers:
RendererFeatureSupport | Meaning |
|---|---|
Unknown (0) | Not audited or probed. It is also what you get for an invalid feature value. |
Unsupported (1) | Unavailable, and the public operation must refuse deterministically. |
Supported (2) | The complete documented contract is available. |
Restricted (3) | Only a structured, explicitly described subset is available; the feature's note says which. |
What you will actually see at this snapshot. GraphicsDevice builds every detailed feature from a boolean renderer query, so its answers are Supported or Unsupported. Unknown and Restricted are defined in the C++ enumeration and in the C ABI, but nothing in the device builds them yet. Write your code to handle all four anyway; a later snapshot is free to use them.
The 14 features without a GraphicsCapability twin, and what each one promises (the ids are not contiguous: retired ids stay reserved, so the surviving values keep their numbers):
| Id | RendererFeature | Meaning |
|---|---|---|
| 8 | ShaderEffectSourceExecution | The source you supply to a ShaderEffect determines the pixels. ShaderEffects (id 7) only says the object is accepted; a renderer can accept it and still use a fixed shader path, which is why this is a separate feature. |
| 21 | ShadowSampling | Lit stock and PBR shaders sample the configured shadow state. |
| 22 | ImageBasedLighting | PBR shaders consume the configured image-based-lighting resources. |
| 24–29 | ShaderDialectGlslDesktop, ShaderDialectGlslEs, ShaderDialectGlslVulkan, ShaderDialectHlsl, ShaderDialectMsl, ShaderDialectWgsl | Which source dialect a ShaderEffect must be written in. The device sets at most one of them, and only when the renderer executes effect source. |
| 30 | Texture3DSampling | A Texture3D bound to a custom effect is sampled by that shader. Texture3DStorage (id 9) is only upload and read-back; a renderer can store volumes faithfully and still have no 3D sampler path. |
| 31 | BaseInstanceDrawing | Instanced indexed drawing can start at a caller-selected instance (SDL_GPU is the only renderer that implements it at this snapshot). |
| 32 | SamplerLevelOfDetailBias | SamplerState.MipMapLevelOfDetailBias changes which mip level is sampled. Unsupported here means the bias is accepted and has no effect, never refused. |
| 33–34 | PbrEffectShading, SkinnedPbrEffectShading | The renderer shades PbrEffect / SkinnedPbrEffect with its metallic-roughness shaders. Unsupported means those effects draw an untextured, unlit fallback. |
| 35 | MultiSampleMask | BlendState/GraphicsDevice MultiSampleMask selects which samples of a multisampled target a draw writes. Restricted (SDL_GPU) means only a mask keeping every sample or none is applied and any other throws NotSupportedException; Unsupported means the mask has no effect. A single-sample target ignores the mask, as in XNA. |
| 36 | RenderTargetReadback | Texture2D.GetData on a RenderTarget2D returns the drawn pixels. Unsupported (HEADLESS, STUB) means that GetData throws NotSupportedException. |
Some entries carry a note, an English qualification from the device. The device attaches one to, among others, ShaderEffects, ShaderEffectSourceExecution, Texture3DStorage, Texture3DSampling and Float16TextureLinearFiltering. The last one is worth reading: its note says the renderer can filter half-float textures, while XNA draws keep XNA's point-filter-only rule for those formats.
Numeric limits
RendererLimitValue is { bool known; std::uint64_t value; }. Always test known; value is meaningful only when it is true. Which entries are known is itself information: MaxColorAttachments, for example, is known on the EasyGL identities, SDL_GPU and WEBGPU and unknown elsewhere, and MaxVertexStreams is reported as a real 0 on a device with no 3D pipeline. The ids are not contiguous, for the same reason as the features’.
| Id | RendererLimit | Meaning |
|---|---|---|
| 0 | MaxTextureDimension | Maximum width or height of a two-dimensional texture |
| 1 | MaxVertexStreams | Same-rate vertex streams in one draw (0 with no 3D pipeline, 1 without multi-stream input) |
| 11 | MaxUniformBufferBytes | Byte range addressable through one uniform or constant-buffer binding |
| 14 | MaxSampledTexturesPerShaderStage | Sampled textures visible to one shader stage |
| 16 | MaxVertexInputBindings | Native vertex-buffer bindings consumed by one draw |
| 17 | MaxVertexInputAttributes | Vertex attributes consumed by one draw |
| 18 | MaxColorAttachments | Colour attachments written by one graphics draw |
| 20 | MinUniformBufferOffsetAlignment | Required byte alignment of a uniform or constant-buffer binding offset |
Per-format usage
The catalogue covers all 27 SurfaceFormat values: the 20 classic XNA formats and CNA's seven extensions (ColorBgraEXT, ColorSrgbEXT, Dxt5SrgbEXT, Bc7EXT, Bc7SrgbEXT, ByteEXT, UShortEXT). For each one RendererFormatSupport holds two bit masks over RendererFormatUsage:
knownUsages— the usages the renderer has classified.supportedUsages— the usages that are supported. It is always a subset of the known mask.
A bit that is not in knownUsages means “not classified”, not “unsupported”. Do not infer support or rejection for an unclassified usage from the renderer name or from another usage bit. RendererFormatSupport::Supports(usage) is true only for a usage that is both known and supported, and IsKnown(usage) tells the difference; with a combined mask, both require every bit.
The device always classifies three usages for every format: TextureStorage, RenderTarget and ColorTransfer. The remaining seven come from the renderer, and only renderers that report them contribute known bits.
| Bit | RendererFormatUsage | Meaning |
|---|---|---|
| 0 | TextureStorage | Faithful Texture2D storage in this format |
| 1 | Sampled | Sampled in a graphics shader |
| 2 | Filterable | Linear or mip filtering while sampling |
| 3 | RenderTarget | A render target in this format can be created and bound |
| 4 | Blendable | Graphics output can be blended into it |
| 8–9 | TransferSource, TransferDestination | Copying from or into a resource of this format |
| 10 | Mipmapped | More than one mip level |
| 11 | Multisample | A multisampled image in this format |
| 12 | ColorTransfer | Transfer through a Color-shaped element |
RendererFormatUsage values combine with operator|, which the header provides.
The English report
GetRendererCapabilityReportEXT() returns the whole profile as UTF-8 text owned by the cached snapshot, so it is cheap to call repeatedly and safe to print at startup or to attach to a bug report. Its shape is fixed:
Renderer capability report
Renderer: <name>
Profile schema: 3
Detailed features
- <FeatureName>: <unknown|unsupported|supported|restricted> -- <description> [Note: <note>]
...
Numeric limits
- <LimitName>: <value|unknown>
...
Surface-format support
- <FormatName>: texture-storage=yes, render-target=no, ... (only known usages are listed)
...
Additional limitations
<text>
The closing “Additional limitations” block is a standing caveat worth quoting to your users. It states that the machine-readable entries describe individual contracts, not that every combination of supported features is valid; that unknown format-usage bits are deliberately not treated as unsupported; that runtime-probed answers belong to this GraphicsDevice and may change after renderer or device reconstruction; that native-versus-emulated delivery and performance are not inferred; and that testing on one driver or compatibility layer is not certification of untested hardware. The device appends renderer-specific sentences, for instance when a renderer accepts ShaderEffect objects but its source does not determine the pixels.
Dump a device's profile
This program prints every feature and limit of whichever renderer your build selected, then exits after one frame. Use it with any of the 10 renderers; it is a good first thing to run on a machine you have not tried yet.
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "Microsoft/Xna/Framework/Graphics/SurfaceFormat.hpp"
#include "CNA/RendererCapabilityProfile.hpp"
#include <cstdio>
#include <string_view>
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
namespace
{
const char* SupportText(CNA::RendererFeatureSupport support)
{
switch (support)
{
case CNA::RendererFeatureSupport::Unsupported: return "unsupported";
case CNA::RendererFeatureSupport::Supported: return "supported";
case CNA::RendererFeatureSupport::Restricted: return "restricted";
case CNA::RendererFeatureSupport::Unknown: break;
}
return "unknown";
}
void DumpProfile(const GraphicsDevice& device)
{
// A reference into the device's cached snapshot; it stays valid until the renderer is
// rebuilt (Reset with a new MSAA count, device recreation) or the device is destroyed.
const CNA::RendererCapabilityProfile& profile = device.GetRendererCapabilityProfileEXT();
std::printf("profile for %.*s\n", static_cast<int>(profile.GetRendererName().size()),
profile.GetRendererName().data());
for (const CNA::RendererFeature feature : CNA::AllRendererFeatures())
{
const CNA::RendererFeatureInfo& info = profile.GetFeature(feature);
const std::string_view name = CNA::GetRendererFeatureName(feature);
std::printf(" %-32.*s %s%s%s\n", static_cast<int>(name.size()), name.data(),
SupportText(info.support), info.note.empty() ? "" : " // ",
info.note.c_str());
}
for (const CNA::RendererLimit limit : CNA::AllRendererLimits())
{
const CNA::RendererLimitValue value = profile.GetLimit(limit);
const std::string_view name = CNA::GetRendererLimitName(limit);
if (value.known)
std::printf(" %-40.*s %llu\n", static_cast<int>(name.size()), name.data(),
static_cast<unsigned long long>(value.value));
else
std::printf(" %-40.*s unknown\n", static_cast<int>(name.size()), name.data());
}
}
}
class ProfileProbe final : public Game
{
public:
ProfileProbe() : graphics_(this) {}
protected:
void Initialize() override
{
Game::Initialize();
DumpProfile(getGraphicsDeviceProperty());
}
void Update(GameTime& gameTime) override
{
(void)gameTime;
Exit(); // a probe needs one frame, not a game loop
}
private:
GraphicsDeviceManager graphics_;
};
int main()
{
ProfileProbe probe;
probe.Run();
return 0;
}
Build it as a tiny CMake project that calls add_subdirectory on your cna checkout, links the CNA target and sets CNA_GRAPHICS_RENDERER before adding CNA. To see the API explain the difference between renderers, configure the same project twice with different renderers and compare the output. To print the full text instead of the loop, take the std::string_view from device.GetRendererCapabilityReportEXT() and write it out with std::fwrite(report.data(), 1, report.size(), stdout); a std::string_view is not guaranteed to end in a null terminator, so avoid printf("%s", …) on it.
Status. We checked every name and signature in this tutorial against the headers and sources at snapshot 3b43bd86. Earlier versions of these fragments compiled as a syntax-only check against the headers of snapshot c1c316b9; the fragments changed for this snapshot have not been compiled. We did not build or run CNA for this page, so we show the shape of the output rather than sample values: the values depend on your renderer and driver.
Make decisions from the profile
Two typical uses. First, choosing an HDR path. Ask the cheap boolean form for the feature, then ask the per-format form for the exact usages you need, and remember the “known” rule:
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "Microsoft/Xna/Framework/Graphics/SurfaceFormat.hpp"
#include "CNA/RendererCapabilityProfile.hpp"
using namespace Microsoft::Xna::Framework::Graphics;
bool CanUseHdrTargets(const GraphicsDevice& device)
{
// The cheap yes/no forms: true only for an unqualified Supported answer.
if (!device.SupportsRendererFeatureEXT(CNA::RendererFeature::Float16RenderTargets))
return false;
// Per-format detail: a bit that is not in knownUsages means "not classified", not "no".
const CNA::RendererFormatSupport hdr =
device.GetRendererSurfaceFormatSupportEXT(SurfaceFormat::HalfVector4);
return hdr.Supports(CNA::RendererFormatUsage::RenderTarget |
CNA::RendererFormatUsage::Blendable);
}
Here Blendable is only true on renderers that report it, so a renderer that has not classified blending yields false from Supports() even though it might work. If you would rather treat “unclassified” as “try it”, test IsKnown() first and decide explicitly.
Second, reading a numeric limit together with its feature. The example sizes a G-buffer: check the feature, then read the colour-attachment limit and its known flag, falling back to XNA’s HiDef ceiling of four where the renderer reports no number:
// (same includes and using-directive as the previous snippet)
int GBufferTargetCount(const GraphicsDevice& device)
{
// False under Reach: the device combines the renderer's answer with the XNA profile.
if (!device.SupportsRendererFeatureEXT(CNA::RendererFeature::MultipleRenderTargets))
return 1;
const CNA::RendererLimitValue limit =
device.GetRendererLimitEXT(CNA::RendererLimit::MaxColorAttachments);
if (!limit.known || limit.value >= 4)
return 4; // XNA's HiDef ceiling
return static_cast<int>(limit.value);
}
Read the profile once, after the device exists (Initialize() or later), and decide your render path there. Do not build a per-frame habit out of it; the value is cached, but the branch belongs in setup code.
A profile is a claim, not a proof. Each answer is an explicit mapping from a renderer or device query that the renderer author wrote and, in many cases, tested. It does not certify a feature on hardware nobody tried, and it says nothing about performance. The site's capability-reporting section and Tutorial 101 explain where inherited defaults still make a bare true soft.
The same data through the C API
The experimental C API exposes the profile through five functions declared in CNA/C/graphics.h. The identities are CNA_RENDERER_FEATURE_* (0–34, with reserved gaps), CNA_RENDERER_FEATURE_SUPPORT_* (UNKNOWN, UNSUPPORTED, SUPPORTED, RESTRICTED), CNA_RENDERER_LIMIT_* (0–20, with reserved gaps), CNA_SURFACE_FORMAT_* and the usage bits CNA_RENDERER_FORMAT_USAGE_*.
| Function | Result |
|---|---|
cna_graphics_device_get_renderer_feature_support_ext | One CNA_RENDERER_FEATURE_SUPPORT_* answer |
cna_graphics_device_get_renderer_limit_ext | out_known and out_value (zero when unknown) |
cna_graphics_device_get_surface_format_support_ext | Known and supported usage masks; an absent known bit means unknown |
cna_graphics_device_get_capability_report_size_ext | Exact byte count of the report, without a terminator |
cna_graphics_device_copy_capability_report_ext | Copies the report; CNA_RESULT_BUFFER_TOO_SMALL if your buffer is short, and no partial text is written |
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "CNA/C/graphics.h"
/* graphics_device is the callback-scoped, borrowed handle your host received. */
static void report_profile(CNA_Handle graphics_device)
{
CNA_RendererFeatureSupport pbr = CNA_RENDERER_FEATURE_SUPPORT_UNKNOWN;
if (cna_graphics_device_get_renderer_feature_support_ext(
graphics_device, CNA_RENDERER_FEATURE_PBR_EFFECT_SHADING, &pbr) == CNA_RESULT_SUCCESS)
{
printf("PBR effect shading: %u\n", (unsigned)pbr);
}
CNA_Bool known = CNA_FALSE;
uint64_t max_dimension = 0;
if (cna_graphics_device_get_renderer_limit_ext(
graphics_device, CNA_RENDERER_LIMIT_MAX_TEXTURE_DIMENSION, &known, &max_dimension)
== CNA_RESULT_SUCCESS && known)
{
printf("max texture dimension: %llu\n", (unsigned long long)max_dimension);
}
CNA_RendererFormatUsageFlags known_usages = 0, supported_usages = 0;
if (cna_graphics_device_get_surface_format_support_ext(
graphics_device, CNA_SURFACE_FORMAT_HALF_VECTOR4, &known_usages, &supported_usages)
== CNA_RESULT_SUCCESS)
{
const CNA_RendererFormatUsageFlags rt = CNA_RENDERER_FORMAT_USAGE_RENDER_TARGET;
if ((known_usages & rt) != 0 && (supported_usages & rt) != 0)
printf("HalfVector4 render target: yes\n");
}
uint64_t bytes = 0;
if (cna_graphics_device_get_capability_report_size_ext(graphics_device, &bytes)
== CNA_RESULT_SUCCESS)
{
char* report = (char*)malloc((size_t)bytes + 1);
if (report != NULL
&& cna_graphics_device_copy_capability_report_ext(
graphics_device, report, bytes, &bytes) == CNA_RESULT_SUCCESS)
{
report[bytes] = '\0';
puts(report);
}
free(report);
}
}
The C API is experimental and this fragment is source-level only. CNA's C ABI is 0.50.0 at this snapshot and its own release gate reports it as not ready; no CI job builds the C API library (CNA's own records show local Linux builds, and an earlier macOS build), and we did not build or link it. The fragment above was syntax-checked against the public header, nothing more. Of the language bindings, only C# (CNA.NET, which admits exactly ABI 0.47.0 and so does not load this snapshot’s library) is maintained; the others are archived in CNA Cemetery against older ABIs, so do not assume a binding exposes these functions.
Next steps
- Tutorial 101: Querying renderer capabilities — the 17
GraphicsCapabilitymembers and what a baretruemeans - Tutorial 102: The OpenGL family — renderers whose MSAA, wireframe and float-target answers depend on the granted context
- Tutorial 131: The SDL_GPU renderer and Tutorial 132: WebGPU
- Tutorial 72: Choosing a renderer
- Renderers reference: capability reporting
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 17 contract questions, renderer versus device polarity, known report mismatches, the unsupported-3D policy, four interface-default failure shapes and portable draw claims.