Tutorial 133: Read the Renderer Capability Profile

CNA Tutorials  ·  CNA snapshot 3b43bd86

ℹ

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).

RendererFeatureSame as GraphicsCapability
ThreeDimensionalPipelineThreeD
DepthStencilBuffer, MultiSampleAntiAliasing, MultipleRenderTargets, AnisotropicFiltering, MultiStreamVertexInput, StencilBuffer, AdditiveBlendingthe member of the same name
WireFrameRasterizationWireFrame
OcclusionQueriesOcclusionQuery
ShaderEffectsCustomEffects
Texture3DStorageTexture3D
InstancedDrawingInstancing
CompiledXnaEffectsCompiledEffects
Float32RenderTargets, Float16RenderTargetsFloatRenderTargets, HalfFloatRenderTargets
Float16TextureLinearFilteringHalfFloatTextureLinearFiltering

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:

RendererFeatureSupportMeaning
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):

IdRendererFeatureMeaning
8ShaderEffectSourceExecutionThe 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.
21ShadowSamplingLit stock and PBR shaders sample the configured shadow state.
22ImageBasedLightingPBR shaders consume the configured image-based-lighting resources.
24–29ShaderDialectGlslDesktop, ShaderDialectGlslEs, ShaderDialectGlslVulkan, ShaderDialectHlsl, ShaderDialectMsl, ShaderDialectWgslWhich source dialect a ShaderEffect must be written in. The device sets at most one of them, and only when the renderer executes effect source.
30Texture3DSamplingA 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.
31BaseInstanceDrawingInstanced indexed drawing can start at a caller-selected instance (SDL_GPU is the only renderer that implements it at this snapshot).
32SamplerLevelOfDetailBiasSamplerState.MipMapLevelOfDetailBias changes which mip level is sampled. Unsupported here means the bias is accepted and has no effect, never refused.
33–34PbrEffectShading, SkinnedPbrEffectShadingThe renderer shades PbrEffect / SkinnedPbrEffect with its metallic-roughness shaders. Unsupported means those effects draw an untextured, unlit fallback.
35MultiSampleMaskBlendState/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.
36RenderTargetReadbackTexture2D.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’.

IdRendererLimitMeaning
0MaxTextureDimensionMaximum width or height of a two-dimensional texture
1MaxVertexStreamsSame-rate vertex streams in one draw (0 with no 3D pipeline, 1 without multi-stream input)
11MaxUniformBufferBytesByte range addressable through one uniform or constant-buffer binding
14MaxSampledTexturesPerShaderStageSampled textures visible to one shader stage
16MaxVertexInputBindingsNative vertex-buffer bindings consumed by one draw
17MaxVertexInputAttributesVertex attributes consumed by one draw
18MaxColorAttachmentsColour attachments written by one graphics draw
20MinUniformBufferOffsetAlignmentRequired 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.

BitRendererFormatUsageMeaning
0TextureStorageFaithful Texture2D storage in this format
1SampledSampled in a graphics shader
2FilterableLinear or mip filtering while sampling
3RenderTargetA render target in this format can be created and bound
4BlendableGraphics output can be blended into it
8–9TransferSource, TransferDestinationCopying from or into a resource of this format
10MipmappedMore than one mip level
11MultisampleA multisampled image in this format
12ColorTransferTransfer 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_*.

FunctionResult
cna_graphics_device_get_renderer_feature_support_extOne CNA_RENDERER_FEATURE_SUPPORT_* answer
cna_graphics_device_get_renderer_limit_extout_known and out_value (zero when unknown)
cna_graphics_device_get_surface_format_support_extKnown and supported usage masks; an absent known bit means unknown
cna_graphics_device_get_capability_report_size_extExact byte count of the report, without a terminator
cna_graphics_device_copy_capability_report_extCopies 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