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 19 GraphicsCapability members. This tutorial documents CNA snapshot 009d40f5 (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 large a compute work group may be, 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: 32 features (each with a four-state answer and an optional English note), 22 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()
Nineteen of the 32 features are the 19 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 nineteen — including the device-level derivations Tutorial 101 describes (the float-target answers come from render-target format support, compute and indirect drawing from dedicated renderer queries, and MultipleRenderTargets is combined with the XNA graphics profile: Reach allows one target, HiDef four).
RendererFeature | Same as GraphicsCapability |
|---|---|
ThreeDimensionalPipeline | ThreeD |
DepthStencilBuffer, MultiSampleAntiAliasing, MultipleRenderTargets, AnisotropicFiltering, MultiStreamVertexInput, StencilBuffer, AdditiveBlending, ComputeShaders | the member of the same name |
WireFrameRasterization | WireFrame |
OcclusionQueries | OcclusionQuery |
ShaderEffects | CustomEffects |
Texture3DStorage | Texture3D |
InstancedDrawing | Instancing |
CompiledXnaEffects | CompiledEffects |
Float32RenderTargets, Float16RenderTargets | FloatRenderTargets, HalfFloatRenderTargets |
Float16TextureLinearFiltering | HalfFloatTextureLinearFiltering |
IndirectDrawing | IndirectDraw |
The other thirteen features have no GraphicsCapability member. They are the reason the profile exists: ShaderEffectSourceExecution, Texture3DSampling, ComputeImageBinding, ShadowSampling, ImageBasedLighting, GpuTimers, BaseInstanceDrawing 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 13 features without a GraphicsCapability twin, and what each one promises:
| 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. |
| 19 | ComputeImageBinding | A two-dimensional texture can be bound as a compute image. Requires compute. |
| 21 | ShadowSampling | Lit stock and PBR shaders sample the configured shadow state. |
| 22 | ImageBasedLighting | PBR shaders consume the configured image-based-lighting resources. |
| 23 | GpuTimers | GPU timestamp queries can measure a command range. |
| 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. |
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 answer is the renderer's own fact, used by the engine layer's passes, and that an ordinary XNA draw still follows 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: for example, when a renderer has no compute the three work-group-count limits, three work-group-size limits and the invocation limit are reported known, with value 0, because “the limit is zero” is a real answer there, while a renderer that supports compute but did not report a size leaves it unknown.
| 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) |
| 2–4 | MaxComputeWorkGroupCountX, Y, Z | Maximum compute work-group count per axis |
| 5–7 | MaxComputeWorkGroupSizeX, Y, Z | Maximum compute local size per axis |
| 8 | MaxComputeWorkGroupInvocations | Maximum product of the local sizes |
| 9 | MaxVertexShaderStorageBlocks | Storage-buffer bindings a vertex shader can read |
| 10 | MaxStorageBufferBytes | Byte range addressable through one storage-buffer binding |
| 11 | MaxUniformBufferBytes | Byte range addressable through one uniform or constant-buffer binding |
| 12 | MaxComputeStorageBufferBindings | Storage-buffer bindings visible to one compute shader |
| 13 | MaxTextureArrayLayers | Layers of a sampled two-dimensional texture array |
| 14 | MaxSampledTexturesPerShaderStage | Sampled textures visible to one shader stage |
| 15 | MaxStorageImagesPerShaderStage | Storage images 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 |
| 19 | MinStorageBufferOffsetAlignment | Required byte alignment of a storage-buffer binding offset |
| 20 | MinUniformBufferOffsetAlignment | Required byte alignment of a uniform or constant-buffer binding offset |
| 21 | TimestampPeriodPicoseconds | GPU timestamp tick duration in picoseconds; zero when unavailable |
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 ten 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 |
| 5–7 | StorageRead, StorageWrite, StorageAtomic | Compute or storage-image read, write and atomic operations |
| 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: 2
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 25 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 the way Tutorial 130 builds its probe: 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 009d40f5, and the C++ and C fragments here compile as a syntax-only check against those headers. 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, sizing a compute dispatch. Check the feature, then read the limit and its known flag:
// (same includes and using-directive as the previous snippet)
bool CanDispatchCompute(const GraphicsDevice& device, std::uint64_t& maxInvocations)
{
if (device.GetRendererFeatureSupportEXT(CNA::RendererFeature::ComputeShaders) !=
CNA::RendererFeatureSupport::Supported)
return false;
const CNA::RendererLimitValue limit =
device.GetRendererLimitEXT(CNA::RendererLimit::MaxComputeWorkGroupInvocations);
if (!limit.known) return false;
maxInvocations = limit.value;
return true;
}
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–31), CNA_RENDERER_FEATURE_SUPPORT_* (UNKNOWN, UNSUPPORTED, SUPPORTED, RESTRICTED), CNA_RENDERER_LIMIT_* (0–21), 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 compute = CNA_RENDERER_FEATURE_SUPPORT_UNKNOWN;
if (cna_graphics_device_get_renderer_feature_support_ext(
graphics_device, CNA_RENDERER_FEATURE_COMPUTE_SHADERS, &compute) == CNA_RESULT_SUCCESS)
{
printf("compute shaders: %u\n", (unsigned)compute);
}
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.29.0 at this snapshot and its own release gate reports it as not ready; no CI job builds the C API library, and we did not build or link it. The fragment above was syntax-checked against the public header, nothing more. The public language bindings target an older ABI (0.21.x) and most of them refuse 0.29.0 by an exact-version rule, so do not assume a binding exposes these functions.
Next steps
- Tutorial 101: Querying renderer capabilities — the 19
GraphicsCapabilitymembers and what a baretruemeans - Tutorial 130: Run CNA on desktop OpenGL 4 — a renderer whose compute, indirect-draw 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 19 contract questions, renderer versus device polarity, known report mismatches, the unsupported-3D policy, four interface-default failure shapes and portable draw claims.