Tutorial 133: Read the Renderer Capability Profile

CNA Tutorials  ·  CNA snapshot 009d40f5

ℹ

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

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

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:

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 13 features without a GraphicsCapability twin, and what each one promises:

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.
19ComputeImageBindingA two-dimensional texture can be bound as a compute image. Requires compute.
21ShadowSamplingLit stock and PBR shaders sample the configured shadow state.
22ImageBasedLightingPBR shaders consume the configured image-based-lighting resources.
23GpuTimersGPU timestamp queries can measure a command range.
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.

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.

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)
2–4MaxComputeWorkGroupCountX, Y, ZMaximum compute work-group count per axis
5–7MaxComputeWorkGroupSizeX, Y, ZMaximum compute local size per axis
8MaxComputeWorkGroupInvocationsMaximum product of the local sizes
9MaxVertexShaderStorageBlocksStorage-buffer bindings a vertex shader can read
10MaxStorageBufferBytesByte range addressable through one storage-buffer binding
11MaxUniformBufferBytesByte range addressable through one uniform or constant-buffer binding
12MaxComputeStorageBufferBindingsStorage-buffer bindings visible to one compute shader
13MaxTextureArrayLayersLayers of a sampled two-dimensional texture array
14MaxSampledTexturesPerShaderStageSampled textures visible to one shader stage
15MaxStorageImagesPerShaderStageStorage images 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
19MinStorageBufferOffsetAlignmentRequired byte alignment of a storage-buffer binding offset
20MinUniformBufferOffsetAlignmentRequired byte alignment of a uniform or constant-buffer binding offset
21TimestampPeriodPicosecondsGPU 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.

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
5–7StorageRead, StorageWrite, StorageAtomicCompute or storage-image read, write and atomic operations
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: 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_*.

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