3D Rendering
Implementation status: at this snapshot (CNA c1c316b9, a development snapshot after alpha.1) CNA exposes 14 renderer identities across 12 implementation families. Eleven of them rasterise 3D, one (SDL_RENDERER) is deliberately 2D-only, and HEADLESS and STUB produce no pixels. A normal build compiles one identity selected with CNA_GRAPHICS_RENDERER; a multi-renderer build compiles a compatible list with CNA_GRAPHICS_RENDERERS (the default renderer must be a member of the list) and lets you select one before the first graphics device is created. 3D depth varies by renderer, so use the capability table and the complete renderer reference rather than assuming every selectable identity implements the full pipeline.
The default GraphicsProfile is Reach, and it is enforced on every renderer. Under the default profile 32-bit indices, more than one render target, OcclusionQuery, Texture3D, cube maps above 512, textures above 2048, float and HDR render-target formats, separate alpha blending and GetBackBufferData throw (or, for render-target formats, silently fall back to Color) until you request GraphicsProfile::HiDef. Several examples on this page need it; each says so. See Graphics profile and Tutorial 152.
Surface formats are renderer-qualified. They are gated twice. The graphics profile comes first: Reach refuses eleven SurfaceFormat values for textures and render targets, and HiDef refuses none of the 20 XNA formats. Then the renderer answers for itself: eight renderer families classify formats and every other renderer defers to the framework’s SurfaceFormat::Color-only rule. See Surface format boundaries.
Overview
CNA exposes XNA 4.0’s full 3D pipeline through the Microsoft::Xna::Framework::Graphics namespace. The pipeline follows the same pattern as the original API: you upload geometry into VertexBuffer and IndexBuffer objects, configure a GraphicsDevice render state, apply an Effect, and issue a draw call. In the snippets below gd is the GraphicsDevice& that a Game gets from getGraphicsDeviceProperty(), and presenting is left to Game: it presents in EndDraw, so Draw() never calls Present().
The three EasyGL identities (OPENGLES3, OPENGL33, WEBGL2) share one implementation but select distinct OpenGL ES, desktop OpenGL or WebGL profiles. Vulkan is a separate native renderer and is not an Emscripten identity. Shader dialect and 3D capability follow the active renderer, so use GraphicsDevice capability queries (Tutorial 133) and the renderer reference rather than translating one GL example mechanically to every family.
One identity is deliberately 2D-only — SDL_RENDERER — while HEADLESS and STUB produce no presented pixels. Their exact exception and capability behavior is not interchangeable. By default a 3D call on the 2D-only renderer throws (Unsupported3DGraphicsCallBehavior::Throw); SDL_RENDERER can instead log once per method and return a null object if you call GraphicsDevice::SetUnsupported3DGraphicsCallBehavior(WarnAndStub). STUB is the extreme case: every capability reads false and every call is a silent no-op. The renderer reference is the canonical inventory; this page concentrates on the game-facing 3D API.
Headless and Software sit outside the usual comparison. Headless validates arguments and counts or traces calls (CNA_HEADLESS_MODE is Fast, Validation by default, or Trace) but rasterises nothing. It owns no back-buffer pixels, so GetBackBufferData throws NotSupportedException rather than inventing a frame, and it exists for fast CI of game logic. Software is a genuine CPU rasterizer — real edge functions, a real Z-buffer and perspective-correct 1/w interpolation — intended for deterministic pixel tests rather than real-time gameplay, and it needs no GPU at all. It is no longer as narrow as it once was: its general draw path accepts triangle lists and strips, line lists and strips and point lists, and it reports real multiple render targets (up to four), occlusion queries, instancing, multi-stream input, 4x MSAA and float render targets. It still reports CustomEffects false (it accepts a ShaderEffect but never executes its source), and its PbrEffect path is a reduced CPU cross-check.
| Renderer | 3D support | Notes |
|---|---|---|
EasyGL (OPENGLES3, OPENGL33, WEBGL2) |
Declared production / supported | The most widely used 3D path and the 3D reference for the rest of this page: three identities, one implementation. All three have multiple render targets, occlusion queries (exact on OPENGL33, only 0 or 1 on OPENGLES3 and WEBGL2), Texture3D, instancing and multi-stream input. Anisotropy, wireframe, MSAA and float render targets depend on the runtime GL context. Shadow-map and image-based-lighting sampling are supported. OPENGLES3 is the Linux default and WEBGL2 the Emscripten default. |
| Vulkan | Declared production | Capability is read from the physical device: multiple render targets when maxColorAttachments is above one, MSAA from the colour and depth sample masks, stencil from the chosen depth format, float render targets per format. All six stock effects plus PbrEffect and SkinnedPbrEffect. Occlusion queries (exact only with the precise-occlusion feature), instancing, compute, indirect draw, and shadow-map and image-based-lighting sampling where the device allows. A measured software-Vulkan (llvmpipe) capability snapshot is checked into CNA’s tests. |
| DIRECTX9 | Declared production | Windows-only. The renderer CNA diffs against the real-XNA-4.0 reference corpus (recorded at 0 difference, tolerance 0, under Wine + DXVK on Linux; run by hand, not in CI). Uniquely among the renderers it already enforced the Reach ceilings before this snapshot, and it answers GraphicsAdapter::IsProfileSupported from real hardware limits. It declares no capabilities of its own: SupportsCapability returns the shared permissive defaults, so its answers are not measurements. Compiled XNA effects are opt-in (CNA_DIRECTX9_COMPILED_EFFECTS). |
| DIRECTX11 | Declared production | Windows-only. Multiple render targets, occlusion queries, wireframe, instancing, Texture3D, custom (HLSL) ShaderEffect, float and half-float render targets queried from the device, runtime D3DCompile. startIndex and baseVertex draw offsets are honoured. No compute, indirect draw, or shadow-map and image-based-lighting sampling. |
| SDL_GPU | Declared supported | Built on SDL3’s GPU API: Vulkan natively, and Direct3D 12 or Metal devices through SDL_shadercross, which the build files turn on by default on Windows and Apple (on macOS, SDL’s Metal driver passed CNA’s full test tree on a physical Mac mini M4 — 10,962 tests with named skips only, a local run with no CI lane; the Windows route has no native run). Multiple render targets (4), instancing, compute, indirect draw, float render targets, shadow-map and image-based-lighting sampling. No occlusion queries (the vendored SDL_gpu has no query API). A custom ShaderEffect only where the build found libshaderc (never on Windows, Apple or Emscripten), and ShaderEffect instancing is not implemented. |
| WebGPU | Declared experimental | Native (wgpu-native) and, at this snapshot, in the browser through Emscripten’s WebGPU port. Real multiple render targets (2 to 4 RenderTarget2Ds), occlusion queries (exact), wireframe (by edge expansion), instancing, multi-stream input, stencil, cube and volume textures, probed 4x MSAA and probed float render targets, compute, and shadow-map and image-based-lighting sampling. No half-float linear filtering, and no packed 16-bit texture formats. |
| FNA3D | Declared experimental | Adapter over the FNA3D C library, which picks SDL_GPU, Direct3D 11 or OpenGL at run time. Compiled D3D9 Effect Framework binaries are always available and are its only shader route (CustomEffects is false); PbrEffect and SkinnedPbrEffect draws are refused by name; no float render targets. Instancing follows the selected FNA3D driver. |
| Software | Declared experimental | Genuine CPU rasterizer with a real Z-buffer and perspective-correct interpolation. Multiple render targets (4), 4x MSAA, exact occlusion queries, instancing, float render targets; no ShaderEffect execution; reduced PBR. Needs no GPU. |
| METAL | Supported on macOS and iOS; not primary-production | Native Metal on macOS and iOS/iPadOS: MSAA (2x/4x on an M4), up to eight render targets, occlusion queries, instancing and multi-stream input, every uncompressed XNA surface format with float and half-float render targets, DXT, cube and volume textures, and full PBR including both KHR_materials_specular maps. Custom effects are SpriteBatch-scoped MSL only; compiled XNA effects need -DCNA_METAL_COMPILED_EFFECTS=ON (default OFF); no compute or indirect draws. Evidence: CNA’s full test tree (11,015 tests) and ctest -L Metal 261/261 under Metal’s validation layers on a physical Mac mini M4 with a locked console; on-screen presentation, a physical iOS device and a soak run are unmeasured, so CNA rates it supported, not primary-production (Tutorial 109). |
SDL_RENDERER |
2D only | No programmable 3D pipeline. Every capability that describes 3D, effects or render targets reads false (SDL_RENDERER reports only additive blending). 3D calls throw by default. |
| Headless | Renders nothing | A test harness, not a renderer: it validates and traces calls. For fast CI of game logic. |
| Stub | No-op | Every capability reads false and every call is a silent no-op. The default renderer of CNA’s dev and unit CMake presets. |
The status badges are CNA’s own declared maturity classification (production, supported, experimental), not a measurement, and the capability statements are what each renderer’s code declares for a HiDef device (the default profile, Reach, refuses several of them; see Graphics profile).
Cross-renderer caveats
Three verified issues cut across renderers and are worth knowing before you debug something that looks like your own bug.
| Caveat | Detail |
|---|---|
| The default profile is Reach | CNA enforces XNA’s graphics-profile ceilings in the shared device layer, the same on every renderer, and the default is Reach. 32-bit indices, more than one render target, OcclusionQuery, Texture3D, cubes above 512, textures above 2048, float and HDR render-target formats, separate alpha blending and GetBackBufferData throw System::NotSupportedException (a float render-target request silently becomes Color) until you ask for HiDef. Before this snapshot only DIRECTX9 enforced them, so code that ran elsewhere can now throw. |
SupportsCapability() is not one uniform answer |
GraphicsCapability has 19 members. Six (CompiledEffects, both float render-target entries, HalfFloatTextureLinearFiltering, ComputeShaders and IndirectDraw) are derived by GraphicsDevice from dedicated hooks whose default is false, and MultipleRenderTargets is additionally limited by the profile, so it reads false under Reach on every renderer. The remaining entries fall through to the renderer’s own switch, whose shared default is true for most of them, so a renderer that does not declare its limits can claim more than it does. OcclusionQuery is the renderer’s answer alone: it can read true under Reach while constructing the query still throws. DIRECTX9 is the one renderer with no override at all (DIRECTX11 and SDL_GPU now declare theirs), so its answers are inherited defaults, not measurements. Do not use a bare true as execution proof. |
| 32-bit indices need HiDef and renderer support | Under Reach a 32-bit IndexBuffer or 32-bit user indices throw. On HiDef the renderer must still implement them: every 3D renderer does, while a renderer that does not refuses (the shared default is a std::runtime_error). A draw call may also not exceed 65,535 primitives under Reach (1,048,575 under HiDef). |
Surface format boundaries
The framework no longer has one single answer, and the answer has two parts. First the profile gate: under the default Reach profile, creating a Texture2D in any of eleven formats — Rgba1010102, Rg32, Rgba64, Alpha8, Single, Vector2, Vector4, HalfSingle, HalfVector2, HalfVector4 and HdrBlendable — throws NotSupportedException, and a RenderTarget2D that asks for one silently becomes Color. HiDef refuses none of the 20 XNA formats. Then the renderer gate: Texture2D asks the active renderer for a three-way Supported/Unsupported/Defer verdict. The renderer families in the table below classify formats themselves; every other renderer answers Defer, and Defer falls back to Texture::ValidateFormat, whose public rule accepts SurfaceFormat::Color only. The seven CNAEXT formats (ColorBgraEXT, ColorSrgbEXT, Dxt5SrgbEXT, Bc7EXT, Bc7SrgbEXT, ByteEXT, UShortEXT) are refused for Texture2D on every renderer but one (most answer Defer; DIRECTX11 answers Unsupported where a DXGI mapping exists): WEBGPU classifies Dxt5SrgbEXT, Bc7EXT and Bc7SrgbEXT as Supported when its device enabled BC compression.
| Renderer | Texture2D formats accepted after the profile gate |
|---|---|
DIRECTX11 | All 20 XNA formats, asked of the device (a few, such as Bgra4444, are optional in hardware). |
VULKAN | All 20, mapped to VkFormats; the DXT formats need textureCompressionBC and Bgra4444 needs VK_EXT_4444_formats; then per-format device properties decide. |
SOFTWARE | All 20 (CPU storage). Render targets: Color, Rgba1010102, Rg32, Rgba64 and the float and half-float formats, but not the packed 16-bit, DXT, signed-normalized or Alpha8 formats. |
OPENGLES3, WEBGL2, OPENGL33 (EasyGL) | Color, Alpha8, DXT1/3/5 (decoded on the CPU when S3TC is missing), the packed 16-bit formats, signed-normalized, Rgba1010102 and float/half; Rg32 and Rgba64 only with the 16-bit-norm texture extension. |
SDL_GPU | Color, Bgr565, Bgra5551, Bgra4444, DXT1/3/5, NormalizedByte2/NormalizedByte4, and the float, half, Alpha8, Rg32, Rgba64 and Rgba1010102 formats in exact or four-channel storage with Direct3D 9’s channel fill (SDL_gpu has no texture swizzle); a device that cannot sample the storage refuses the format by name. |
WEBGPU | Color; DXT/BC when the device has TextureCompressionBC; NormalizedByte2/NormalizedByte4; HdrBlendable and HalfVector4 as rgba16float; the three packed 16-bit formats are refused by name; everything else is Color-only. |
METAL | Every uncompressed format natively (float, half, Rgba1010102, 16-bit-norm, signed-normalized, Alpha8; the packed 16-bit formats only on Apple-family GPUs) and DXT1/3/5 (BC on every Mac, decoded to RGBA8 beside the kept blocks where the GPU has no BC). Render targets: Color, Rgba1010102, Rg32, Rgba64 and the float and half-float formats. |
FNA3D, DIRECTX9, HEADLESS, STUB and the 2D-only SDL_RENDERER | Color only. |
The practical consequences:
- Do not infer public support from a renderer’s internal mapping. A renderer that answers Defer still reaches the framework’s
Color-only gate, whatever its native API could do. - Ask, do not guess.
GraphicsDevice::SupportsSurfaceFormatAsRenderTargetEXT(format)combines the profile rule and the renderer’s verdict, and it is exactly the question theRenderTarget2Dconstructor asks, so the two cannot disagree.GetRendererCapabilityProfileEXT()exposes per-format usage masks. - Volumes and cubes are stricter.
Texture3Dis HiDef-only, limited to 15 uncompressed formats (no DXT, no signed-normalized).TextureCubenever accepts the signed-normalized formats on either profile, and only EasyGL,VULKAN,SDL_GPU,WEBGPU,SOFTWAREandMETALhave a cube-specific classifier;DIRECTX11inherits theTexture2Dverdict for cubes (its cube storage carries the requested DXGI format), and the rest areColor-only. No render target is ever a DXT format.
Graphics profile: Reach and HiDef
Every GraphicsDevice runs under a GraphicsProfile, chosen through the GraphicsDeviceManager, which applies it to the device just before Initialize() (and again on ApplyChanges()). The default is Reach, the smaller set; HiDef lifts the ceilings. In CNA the profile is a software ceiling, checked in the shared device layer for every renderer, not a hardware query. What it changes on the API this page describes:
| Rule | Reach (default) | HiDef |
|---|---|---|
Texture2D / RenderTarget2D edge length | 2048 | 4096 |
TextureCube edge length | 512, power of two | 4096 |
Texture3D | refused | 256 per axis |
| Simultaneous render targets | 1 | 4 |
| 32-bit index buffers and user indices | refused | allowed |
| Primitives per draw call | 65,535 | 1,048,575 |
OcclusionQuery | refused | allowed (if the renderer has queries) |
| Float, half-float and packed render-target formats | refused; a request silently becomes Color | allowed (if the renderer can render into them) |
GetBackBufferData | refused | allowed |
Request HiDef in the Game constructor, before Initialize() applies your preferences, and optionally only when the adapter supports it:
// In the Game constructor, before Initialize() applies the preferences:
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
// Optional: only if the adapter says it can
if (GraphicsAdapter::getDefaultAdapterProperty().IsProfileSupported(GraphicsProfile::HiDef))
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
HiDef lifts a ceiling; it does not give a renderer a feature it lacks (see the per-renderer table above and the matrix in Tutorial 152). Tutorial 152 also lists every refusal with its exception message, the project-wide CNA::SetProjectGraphicsProfileEXT alternative, and a probe program.
Vertex types
CNA provides the same four built-in vertex types as XNA 4.0, plus three CNAEXT types (VertexPositionNormalTangentTexture and two skinned variants) used by normal-mapped and skinned content. Every vertex type carries a static VertexDeclaration, returned by getVertexDeclarationStatic(), that describes the layout of its fields to the GPU. You can also define custom vertex structs and supply a matching VertexDeclaration.
| Type | Fields | Typical use |
|---|---|---|
VertexPositionColor |
Position (Vector3), Color (Color) |
Debug geometry, wireframes, simple colored primitives |
VertexPositionTexture |
Position (Vector3), TextureCoordinate (Vector2) |
Textured quads and simple textured meshes without lighting |
VertexPositionColorTexture |
Position (Vector3), Color (Color), TextureCoordinate (Vector2) |
Sprites in 3D space, tinted textured geometry |
VertexPositionNormalTexture |
Position (Vector3), Normal (Vector3), TextureCoordinate (Vector2) |
Lit 3D meshes with BasicEffect, EnvironmentMapEffect, SkinnedEffect |
Each vertex type’s declaration enumerates the VertexElement entries — their semantic (VertexElementUsage), format (VertexElementFormat), and byte offset within the struct. The GraphicsDevice reads this declaration when a vertex buffer is bound to know how to interpret the raw bytes. A declaration may have at most 16 elements and 255 bytes per vertex, and under the default Reach profile its element formats may not go beyond NormalizedShort4.
// Access the declaration for a built-in type
const VertexDeclaration& decl = VertexPositionNormalTexture::getVertexDeclarationStatic();
// Inspect the stride (bytes per vertex)
int stride = decl.getVertexStrideProperty(); // 32 bytes for VertexPositionNormalTexture
Buffer management
VertexBuffer
A VertexBuffer stores a fixed array of vertices in GPU memory. Pass the VertexDeclaration describing the vertex layout, the number of vertices, and a BufferUsage hint when constructing.
// Allocate a static vertex buffer for 3 vertices
// ("gd" is the GraphicsDevice& from Game::getGraphicsDeviceProperty())
auto vb = std::make_unique<VertexBuffer>(
gd,
VertexPositionColor::getVertexDeclarationStatic(),
3,
BufferUsage::None);
// Upload vertex data
std::vector<VertexPositionColor> vertices = {
{ Vector3(0.0f, 0.5f, 0.0f), Color::Red },
{ Vector3(0.5f, -0.5f, 0.0f), Color::Green },
{ Vector3(-0.5f,-0.5f, 0.0f), Color::Blue },
};
vb->SetData(vertices.data(), static_cast<int>(vertices.size()));
// Read back data (only allowed because the usage is None, not WriteOnly)
vb->GetData(vertices.data(), static_cast<int>(vertices.size()));
BufferUsage is a readability hint, not a static/dynamic switch. BufferUsage::WriteOnly promises that you will never read the data back: every GetData call on such a buffer throws System::NotSupportedException, while SetData stays legal. BufferUsage::None allows GetData. Neither carries a CNA-verified speed difference. For data that changes every frame, use DynamicVertexBuffer.
DynamicVertexBuffer
DynamicVertexBuffer is for geometry that changes every frame, such as particle systems or procedurally animated meshes. Written with SetDataOptions::Discard (replace everything) or NoOverwrite (append without touching data in use), its SetData is legal even while the buffer is bound. An ordinary SetData on a buffer that is currently bound on the device (or on a texture bound to a sampler) throws System::InvalidOperationException (“The vertex buffer resource is in use.”). Most renderers honour the options as real hints and a few treat every call as Discard; either way CNA keeps a CPU copy and re-submits the whole buffer, so a partial update saves you CPU work, not upload bandwidth. Tutorial 38 covers the streaming rules.
auto dvb = std::make_unique<DynamicVertexBuffer>(
gd,
VertexPositionColor::getVertexDeclarationStatic(),
maxParticles,
BufferUsage::WriteOnly);
// Each frame: discard old data and upload new
// (arguments: source array, start index in the source, element count, options)
dvb->SetData(particleVerts.data(), 0, static_cast<int>(liveCount),
SetDataOptions::Discard);
IndexBuffer
An IndexBuffer stores integer indices that reference vertices in a VertexBuffer, enabling vertex reuse. Choose IndexElementSize::SixteenBits (up to 65 535 unique vertices per draw) or IndexElementSize::ThirtyTwoBits (up to ~4 billion). 32-bit indices need the HiDef profile: under the default Reach profile a 32-bit IndexBuffer throws NotSupportedException. An index or vertex buffer may not exceed 67,108,863 bytes on either profile.
auto ib = std::make_unique<IndexBuffer>(
gd,
IndexElementSize::SixteenBits,
36, // 12 triangles * 3 indices = 36 for a cube
BufferUsage::None);
std::vector<uint16_t> indices = { 0, 1, 2, 0, 2, 3, /* ... */ };
ib->SetData(indices.data(), static_cast<int>(indices.size()));
Binding and draw calls
Before issuing a draw call you must bind at least one vertex buffer to the GraphicsDevice. Index buffers and the active effect are set in the same way; drawing with no effect applied, or an indexed draw with no index buffer bound, throws System::InvalidOperationException.
gd.SetVertexBuffer(vb.get());
gd.setIndicesProperty(ib.get()); // optional; required for indexed draws
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply(); // upload shader constants
The integer subscript of the pass collection returns a pointer (null when out of range), hence ->Apply(). setIndicesProperty is XNA’s GraphicsDevice.Indices; SetIndexBuffer is a CNAEXT alias.
Draw call variants
| Method | Description | Renderers |
|---|---|---|
DrawPrimitives(type, startVertex, primitiveCount) |
Non-indexed draw from the bound vertex buffer | All 3D renderers |
DrawIndexedPrimitives(type, baseVertex, minVertexIndex, numVertices, startIndex, primitiveCount) |
Indexed draw; combines vertices with index buffer | All 3D renderers |
DrawInstancedPrimitives(type, baseVertex, minVertexIndex, numVertices, startIndex, primitiveCount, instanceCount) |
GPU instancing: draw instanceCount copies in a single call | Renderers that report Instancing: OPENGLES3, WEBGL2, OPENGL33, VULKAN, WEBGPU, SDL_GPU, DIRECTX9, DIRECTX11, SOFTWARE, METAL, and FNA3D (from its driver, and not with stock effects). Not STUB or the 2D-only SDL_RENDERER. |
DrawUserPrimitives(type, vertices, vertexOffset, primitiveCount) |
Immediate-mode draw; no VertexBuffer object needed. Overloads take the four built-in vertex structs, and a std::vector<T> template takes any type that implements IVertexType; a DrawUserIndexedPrimitives family adds an index array. |
All 3D renderers |
Every draw call is limited to 65,535 primitives under the default Reach profile (1,048,575 under HiDef); DrawInstancedPrimitives applies the same limit to its instance count. Exceeding it throws System::NotSupportedException.
DrawUserPrimitives uploads vertex data to a transient buffer on every call. It is convenient for small one-off shapes but inefficient for large or frequently drawn geometry. Prefer VertexBuffer for anything drawn more than once per frame.
PrimitiveType
All draw calls accept a PrimitiveType that tells the GPU how to interpret the vertex stream. XNA front faces are clockwise: the default RasterizerState::CullCounterClockwise culls counter-clockwise triangles, so a mesh wound the other way appears inside-out (see Tutorial 40).
| Value | Vertices per primitive | Description |
|---|---|---|
TriangleList |
3 per triangle | Each group of three vertices forms an independent triangle. Most common for solid meshes. |
TriangleStrip |
1 per additional triangle | First triangle needs 3 vertices; each subsequent triangle reuses the last two, and the winding flips on every other triangle so the strip stays front-facing. Reduces index count for sequential strips. |
LineList |
2 per line | Each pair of vertices forms an independent line segment. Useful for debug overlays. |
LineStrip |
1 per additional segment | Vertices form a continuous polyline. Efficient for curves and paths. |
PointListEXT |
1 per point | CNAEXT (not in XNA 4.0): each vertex is a point. The stock shaders draw one-pixel points; there is no point-size state, so sized points need a custom shader. |
BasicEffect 3D setup
BasicEffect is the standard effect for solid 3D geometry. It supports per-vertex and per-pixel lighting with up to three independent directional lights, a single diffuse texture, per-vertex color tinting, and distance fog. See the Effects System page for the full API reference; the sections below focus on the typical 3D setup pattern.
The minimal setup for a lit, textured mesh:
- Set the three transformation matrices:
setWorldProperty,setViewProperty,setProjectionProperty. - Enable lighting with
EnableDefaultLighting()or configureDirectionalLight0manually. - Enable and assign a texture via
setTextureEnabledProperty(true)andsetTextureProperty(&myTex)(it takes aTexture2D*and does not own it). - Call
getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply()to upload all constants to the GPU. - Issue the draw call.
auto effect = std::make_unique<BasicEffect>(gd);
// Matrices
effect->setWorldProperty(Matrix::getIdentityProperty());
effect->setViewProperty(Matrix::CreateLookAt(
Vector3(0, 2, 5), // camera position
Vector3::Zero, // look-at target
Vector3::Up));
effect->setProjectionProperty(Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4, // 45 degrees
gd.getViewportProperty().getAspectRatioProperty(),
0.1f, 100.0f));
// Lighting
effect->EnableDefaultLighting();
// Texture (a Texture2D*; the effect does not own it)
effect->setTextureEnabledProperty(true);
effect->setTextureProperty(wallTexture);
// Apply and draw
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
gd.DrawIndexedPrimitives(
PrimitiveType::TriangleList,
0, // baseVertex
0, // minVertexIndex
vertexCount, // numVertices referenced
0, // startIndex
indexCount / 3); // primitiveCount
Camera setup
CNA reproduces the XNA 4.0 Matrix factory methods for building camera and projection matrices. The view matrix transforms world-space coordinates into camera space; the projection matrix further transforms camera-space coordinates into clip space. Matrices are row-major and vectors are rows, so a vertex is transformed as vertex * World * View * Projection (see Tutorial 33).
// View matrix: camera at (3, 4, 5) looking at the origin, Y is up
Matrix view = Matrix::CreateLookAt(
Vector3(3.0f, 4.0f, 5.0f), // eye position
Vector3::Zero, // target
Vector3::Up); // up direction
// Perspective projection: 60-degree vertical FOV, standard near/far
float aspectRatio = gd.getViewportProperty().getAspectRatioProperty();
Matrix projection = Matrix::CreatePerspectiveFieldOfView(
MathHelper::ToRadians(60.0f),
aspectRatio,
0.1f, // near plane
500.0f); // far plane
// Orthographic projection (for UI or isometric views)
Matrix ortho = Matrix::CreateOrthographic(
static_cast<float>(gd.getViewportProperty().getWidthProperty()),
static_cast<float>(gd.getViewportProperty().getHeightProperty()),
-1.0f, 1.0f);
CNA uses a right-handed coordinate system with Y-up, matching XNA 4.0, and the Direct3D depth convention: after the perspective divide x and y lie in [-1, 1] and depth in [0, 1]. The near and far planes are in view-space distance units; keep the ratio of farPlane / nearPlane as small as practical to maintain depth buffer precision.
Depth buffer and blend state
Depth buffer
The depth buffer prevents back faces of opaque geometry from overwriting front faces. CNA initialises the GraphicsDevice with depth testing enabled by default (a GraphicsDeviceManager requests DepthFormat::Depth24 unless you change it with setPreferredDepthStencilFormatProperty). You can switch states explicitly when mixing 3D and 2D content in the same frame.
| State preset | Depth test | Depth write | Typical use |
|---|---|---|---|
DepthStencilState::Default |
LessEqual | Yes | Standard opaque 3D geometry |
DepthStencilState::DepthRead |
LessEqual | No | Transparent or decal geometry that tests but does not write depth |
DepthStencilState::None |
Off | No | SpriteBatch, HUD, and 2D overlays drawn after 3D geometry |
// 3D pass: depth enabled
gd.setDepthStencilStateProperty(DepthStencilState::Default);
Draw3DScene();
// 2D overlay pass: depth off so sprites always appear on top.
// (SpriteBatch::Begin applies DepthStencilState::None by itself, and it does NOT
// restore the previous state afterwards, so put the 3D state back before the next 3D draw.)
gd.setDepthStencilStateProperty(DepthStencilState::None);
spriteBatch->Begin();
spriteBatch->DrawString(*font, "Score: 100", Vector2(10, 10), Color::White);
spriteBatch->End();
gd.setDepthStencilStateProperty(DepthStencilState::Default);
Blend state
Transparent 3D geometry requires an additive or alpha-blend state and should be drawn back-to-front after all opaque geometry. BlendState::AlphaBlend is premultiplied (source factor One, destination InverseSourceAlpha), and BasicEffect premultiplies its diffuse colour by its alpha, so set the alpha with setAlphaProperty. Separate alpha blending (different colour and alpha factors) is refused under the default Reach profile.
// Opaque geometry first (depth writes on)
gd.setBlendStateProperty(BlendState::Opaque);
gd.setDepthStencilStateProperty(DepthStencilState::Default);
DrawOpaqueMeshes();
// Transparent geometry last (depth writes off, alpha blend)
gd.setBlendStateProperty(BlendState::AlphaBlend);
gd.setDepthStencilStateProperty(DepthStencilState::DepthRead);
DrawTransparentMeshes(); // caller responsible for back-to-front sorting
RenderTarget2D
Off-screen rendering lets you draw a scene into a texture and then use that texture in a subsequent draw call — for example for shadow maps, reflections, or post-process effects. SurfaceFormat::Color is the portable common denominator; float and HDR formats need the HiDef profile and a renderer that can render into them, as described above.
// Create a 512x512 off-screen target
auto rt = std::make_unique<RenderTarget2D>(
gd, 512, 512,
false, // no mipmaps
SurfaceFormat::Color,
DepthFormat::Depth24);
// Render the scene into the target
gd.SetRenderTarget(rt.get());
gd.Clear(Color::Black);
DrawMirroredScene();
// Restore the back buffer
gd.SetRenderTarget(nullptr);
// Use the render target as a texture in the next pass
effect->setTextureEnabledProperty(true);
effect->setTextureProperty(rt.get()); // RenderTarget2D is-a Texture2D
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
DrawFullScreenQuad();
A render-target request the profile or the renderer refuses does not throw: the constructor silently falls back to SurfaceFormat::Color. Ask first:
// Ask before you rely on a non-Color render target format:
if (gd.SupportsSurfaceFormatAsRenderTargetEXT(SurfaceFormat::HdrBlendable)) {
hdrTarget = std::make_unique<RenderTarget2D>(
gd, 1280, 720, false, SurfaceFormat::HdrBlendable, DepthFormat::Depth24);
}
// Without the check, a request the profile or renderer refuses does not throw:
// the constructor silently falls back to SurfaceFormat::Color.
Model rendering
The Model class represents a complete 3D asset loaded by the ContentManager. It handles mesh hierarchy, bone transforms, and effect binding internally. Load<Model> returns the Model by value, and the minimal draw call is a single line:
// Load<Model> returns the Model by value (see the Model Loading page)
Model model = getContentProperty().Load<Model>("models/house");
model.Draw(worldMatrix, viewMatrix, projectionMatrix);
Model::Draw iterates over all ModelMesh objects within the model, applies each mesh’s accumulated bone transform to the supplied world matrix, and calls the appropriate effect. Each mesh carries its own mesh-part list and associated Effect. For an XNB model built by an XNA-style pipeline this is typically a BasicEffect; for a model imported from glTF, CNA assigns PbrEffect or SkinnedPbrEffect (BasicEffect or SkinnedEffect only for KHR_materials_unlit materials), so a dynamic_cast to BasicEffect skips those parts. Model::Draw requires every part’s effect to implement IEffectMatrices and throws InvalidOperationException otherwise. The Model Loading page covers the formats and the load ladder.
To override the effect on all meshes (for example to apply a custom tint or swap to a SkinnedEffect for animation):
// Tint every BasicEffect part; parts with another effect type (glTF models
// use PbrEffect) are skipped by the dynamic_cast
for (ModelMesh* mesh : model.getMeshesProperty()) {
for (ModelMeshPart* part : mesh->getMeshPartsProperty()) {
auto* be = dynamic_cast<BasicEffect*>(part->getEffectProperty());
if (be) {
be->setEmissiveColorProperty(Vector3(0.1f, 0.0f, 0.0f)); // subtle red tint
}
}
}
model.Draw(world, view, projection);
Skeletal animation drives a skin palette, not the ModelBone hierarchy: CopyAbsoluteBoneTransformsTo returns scene-node-indexed transforms with no inverse bind pose, so it cannot feed SkinnedEffect. Take the palette from AnimationPlayer::GetSkinTransforms() and pass it to the effect before drawing:
// For a skinned model: the palette comes from an AnimationPlayer built once from the
// SkinningData on model.getTagProperty(), started with StartClip() and Update()d each frame
const std::vector<Matrix>& boneTransforms = animationPlayer.GetSkinTransforms();
for (ModelMesh* mesh : model.getMeshesProperty()) {
for (ModelMeshPart* part : mesh->getMeshPartsProperty()) {
Effect* effect = part->getEffectProperty();
if (auto* pbr = dynamic_cast<SkinnedPbrEffect*>(effect)) {
pbr->SetBoneTransforms(boneTransforms); // glTF metallic-roughness skins
} else if (auto* se = dynamic_cast<SkinnedEffect*>(effect)) {
se->SetBoneTransforms(boneTransforms); // unlit or hand-authored skins
}
}
mesh->Draw();
}
Texture3D and TextureCube
Texture3D (volumetric) and TextureCube (six-face cube map) are available on the 3D-capable renderers that implement them. Texture3D is HiDef-only (up to 256 per axis, 15 uncompressed formats) and is refused outright under the default Reach profile; TextureCube is limited to 512 (power of two) under Reach and 4096 under HiDef. TextureCube is used directly by EnvironmentMapEffect for reflection mapping, and Texture3D is useful for volumetric fog, 3D lookup tables (LUTs), and density fields. Both go through the same two-gate format rule as Texture2D (profile, then renderer), so they are no longer Color-only everywhere: a float LUT is constructible on DIRECTX11, the EasyGL identities, VULKAN, METAL and SOFTWARE under HiDef. WEBGPU supports both cube and volume storage, and METAL stores cube and volume textures in every uncompressed format (DXT for cubes but not for volumes, as in XNA).
// Load a cube map through ContentManager (returns the TextureCube by value)
TextureCube skybox = getContentProperty().Load<TextureCube>("textures/skybox");
auto envEffect = std::make_unique<EnvironmentMapEffect>(gd);
envEffect->setEnvironmentMapProperty(&skybox);
envEffect->setEnvironmentMapAmountProperty(1.0f);
envEffect->setFresnelFactorProperty(0.5f);
Advanced topics
GPU instancing
Drawing many identical meshes (trees, rocks, crowd members) can be done in a single GPU call using DrawInstancedPrimitives. The per-instance data (world matrix, colour, etc.) is uploaded in a second vertex buffer bound to stream index 1 with a step frequency of 1 instance rather than 1 vertex. The binding is VertexBufferBinding(buffer, vertexOffset, instanceFrequency), and the offset is in elements, not bytes.
// Second vertex buffer holds one matrix per instance. Stock effects such as
// BasicEffect read the per-instance world matrix as four Vector4 rows in
// TEXCOORD usage indices 1..4 (see Tutorial 60 for the exact declaration).
auto instanceVB = std::make_unique<VertexBuffer>(
gd, instanceDeclaration, // your own VertexDeclaration for the instance data
instanceCount, BufferUsage::WriteOnly);
instanceVB->SetData(instanceMatrices.data(), instanceCount);
// Bind both streams: (buffer, vertexOffset, instanceFrequency)
gd.SetVertexBuffers({
VertexBufferBinding(meshVB.get(), 0, 0), // stream 0: mesh geometry
VertexBufferBinding(instanceVB.get(), 0, 1), // stream 1: one element per instance
});
gd.setIndicesProperty(meshIB.get());
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
gd.DrawInstancedPrimitives(
PrimitiveType::TriangleList,
0, 0, meshVertexCount, 0, triangleCount, instanceCount);
Instancing is reported by EasyGL (OPENGLES3, WEBGL2, OPENGL33), by VULKAN, WEBGPU, SDL_GPU, DIRECTX9, DIRECTX11, SOFTWARE and METAL (multi-stream and base-instance draws included), and by FNA3D when its selected driver has it. It is not available on STUB, and the 2D-only SDL_RENDERER has no instancing path at all. SDL_GPU instances the stock effects but does not implement ShaderEffect instancing. Check gd.SupportsCapability(CNA::GraphicsCapability::Instancing), and remember that a renderer without its own capability declaration (DIRECTX9) answers with the shared default (see Cross-renderer caveats).
Multiple render targets (MRT)
Binding multiple RenderTarget2D objects simultaneously enables G-buffer rendering for deferred shading. It needs GraphicsProfile::HiDef: the Reach limit is one render target, and SetRenderTargets with more throws NotSupportedException (“SetRenderTargets: 2 render targets exceeds GraphicsProfile.Reach’s own maximum of 1”). On HiDef, support then depends on the renderer: multiple render targets are available on the EasyGL identities (up to 4), VULKAN (when the device reports more than one colour attachment), WEBGPU (2 to 4 targets), SDL_GPU (4), DIRECTX9, DIRECTX11, FNA3D, SOFTWARE (4 CPU targets) and METAL (up to 8). They are not available on STUB or the 2D-only SDL_RENDERER. All targets in the set must have the same size and sample count; the stock effects write only the first target, so filling a whole G-buffer needs a custom ShaderEffect or compiled effect; and a cube face inside a multi-target set works on EasyGL, VULKAN, SOFTWARE, FNA3D and METAL but is refused on WEBGPU, SDL_GPU and the Direct3D renderers. Under Reach every render target is also limited to the eight-bit formats, so a float G-buffer needs HiDef as well. Pass a list of bindings to SetRenderTargets:
// Needs graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef) and a renderer with MRT
if (gd.SupportsCapability(CNA::GraphicsCapability::MultipleRenderTargets)) {
gd.SetRenderTargets({
RenderTargetBinding(albedoRT.get()),
RenderTargetBinding(normalRT.get()),
RenderTargetBinding(depthRT.get()),
});
DrawGeometryPass();
// Restore back buffer
gd.SetRenderTarget(nullptr);
}
OcclusionQuery
OcclusionQuery asks the GPU how many samples passed the depth test for a given draw. This is used for techniques like hardware-accelerated lens flares or conditional rendering of expensive effects. It needs HiDef: constructing one under the default Reach profile throws NotSupportedException. Queries are then available on the EasyGL identities (a real count on OPENGL33; only a 0-or-1 result on OPENGLES3 and WEBGL2), VULKAN (an exact count needs the precise-occlusion feature), WEBGPU (a 0-or-1 result on a Metal adapter), DIRECTX9, DIRECTX11, FNA3D (not with its Metal driver), SOFTWARE and METAL (a real count, one query open at a time). They are absent on SDL_GPU and the 2D-only SDL_RENDERER. isPixelCountPreciseEXT() says whether the count is a tally rather than a flag, and getPixelCountProperty() throws until getIsCompleteProperty() is true.
// Needs HiDef (constructing an OcclusionQuery under Reach throws) and a renderer with queries
if (gd.SupportsCapability(CNA::GraphicsCapability::OcclusionQuery)) {
auto query = std::make_unique<OcclusionQuery>(gd);
query->Begin();
DrawProxyGeometry(); // small bounding box or billboard
query->End();
// On a later frame (avoid stalling the GPU by checking immediately)
if (query->getIsCompleteProperty()) {
bool visible = query->getPixelCountProperty() > 0;
}
}
Code examples
Minimal 3D triangle
The smallest complete example: a coloured triangle rendered with BasicEffect and no index buffer. It fits inside the default Reach profile.
// --- Setup (LoadContent) ---
VertexPositionColor verts[3] = {
{ Vector3( 0.0f, 0.5f, 0.0f), Color::Red }, // top
{ Vector3( 0.5f, -0.5f, 0.0f), Color::Green }, // bottom right
{ Vector3(-0.5f, -0.5f, 0.0f), Color::Blue }, // bottom left (top -> right -> left is clockwise: front-facing)
};
auto vb = std::make_unique<VertexBuffer>(
gd,
VertexPositionColor::getVertexDeclarationStatic(),
3, BufferUsage::WriteOnly);
vb->SetData(verts, 3);
auto effect = std::make_unique<BasicEffect>(gd);
effect->VertexColorEnabled = true;
effect->setWorldProperty(Matrix::getIdentityProperty());
effect->setViewProperty(Matrix::CreateLookAt(
Vector3(0, 0, 3), Vector3::Zero, Vector3::Up));
effect->setProjectionProperty(Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4,
gd.getViewportProperty().getAspectRatioProperty(),
0.1f, 100.0f));
// --- Draw (Draw) --- no Present(): Game presents in EndDraw
gd.Clear(Color::CornflowerBlue);
gd.SetVertexBuffer(vb.get());
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, 1);
Textured cube with camera
A complete rotating textured cube using an index buffer and a perspective camera.
// A full textured cube needs 24 vertices (4 per face), because every face has its own UVs.
// Only the front and back faces are written out here.
std::vector<VertexPositionTexture> cubeVerts = {
// front
{ Vector3(-0.5f, 0.5f, 0.5f), Vector2(0, 0) },
{ Vector3( 0.5f, 0.5f, 0.5f), Vector2(1, 0) },
{ Vector3( 0.5f, -0.5f, 0.5f), Vector2(1, 1) },
{ Vector3(-0.5f, -0.5f, 0.5f), Vector2(0, 1) },
// back
{ Vector3( 0.5f, 0.5f, -0.5f), Vector2(0, 0) },
{ Vector3(-0.5f, 0.5f, -0.5f), Vector2(1, 0) },
{ Vector3(-0.5f, -0.5f, -0.5f), Vector2(1, 1) },
{ Vector3( 0.5f, -0.5f, -0.5f), Vector2(0, 1) },
// ... (left, right, top, bottom faces omitted for brevity)
};
// Each triangle is wound CLOCKWISE as seen from outside: XNA front faces are clockwise
std::vector<uint16_t> cubeIdx = {
0,1,2, 0,2,3, // front
4,5,6, 4,6,7, // back
// ...
};
auto cubeVB = std::make_unique<VertexBuffer>(gd,
VertexPositionTexture::getVertexDeclarationStatic(),
static_cast<int>(cubeVerts.size()), BufferUsage::WriteOnly);
cubeVB->SetData(cubeVerts.data(), static_cast<int>(cubeVerts.size()));
auto cubeIB = std::make_unique<IndexBuffer>(gd,
IndexElementSize::SixteenBits,
static_cast<int>(cubeIdx.size()), BufferUsage::WriteOnly);
cubeIB->SetData(cubeIdx.data(), static_cast<int>(cubeIdx.size()));
auto cubeEffect = std::make_unique<BasicEffect>(gd);
cubeEffect->setTextureEnabledProperty(true);
cubeEffect->setTextureProperty(crateTexture);
// In Update(): accumulate the rotation angle (angle_ is a float member)
angle_ += (float)gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty();
// In Draw():
gd.Clear(Color::CornflowerBlue);
gd.setDepthStencilStateProperty(DepthStencilState::Default);
cubeEffect->setWorldProperty(Matrix::CreateRotationY(angle_));
cubeEffect->setViewProperty(Matrix::CreateLookAt(
Vector3(0, 1.5f, 3), Vector3::Zero, Vector3::Up));
cubeEffect->setProjectionProperty(Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4,
gd.getViewportProperty().getAspectRatioProperty(),
0.1f, 100.0f));
gd.SetVertexBuffer(cubeVB.get());
gd.setIndicesProperty(cubeIB.get());
cubeEffect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
gd.DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0,
static_cast<int>(cubeVerts.size()), 0,
static_cast<int>(cubeIdx.size()) / 3);
Model::Draw usage
Load a model asset (an .xnb, a compiled .cnb, a .cnj descriptor, or a glTF/GLB file) through the ContentManager and draw it with a single call.
// LoadContent
Model houseModel = getContentProperty().Load<Model>("models/house");
// Update: animate position or rotation
Matrix houseWorld = Matrix::CreateTranslation(housePos);
// Draw
Matrix view = Matrix::CreateLookAt(cameraPos, cameraTarget, Vector3::Up);
Matrix proj = Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4,
gd.getViewportProperty().getAspectRatioProperty(),
0.5f, 1000.0f);
gd.setDepthStencilStateProperty(DepthStencilState::Default);
gd.setBlendStateProperty(BlendState::Opaque);
houseModel.Draw(houseWorld, view, proj);
Wireframe rendering
Wireframe rendering uses a RasterizerState whose fill mode is FillMode::WireFrame; there is no ready-made wireframe preset, so build one. Availability follows the renderer: WEBGPU, SDL_GPU, DIRECTX9, DIRECTX11, FNA3D, SOFTWARE, METAL and HEADLESS report it, VULKAN reports it when the device supports non-solid fill, and the EasyGL identities report it only where the GL context has a native polygon-mode API (desktop OPENGL33 has one, the ES and WebGL contexts do not).
// Build a wireframe rasteriser state once (there is no RasterizerState::WireFrame preset)
RasterizerState wireframe;
wireframe.setFillModeProperty(FillMode::WireFrame);
wireframe.setCullModeProperty(CullMode::None);
if (gd.SupportsCapability(CNA::GraphicsCapability::WireFrame)) {
gd.setRasterizerStateProperty(wireframe);
effect->VertexColorEnabled = true;
effect->getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
gd.DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, vertexCount, 0, triangleCount);
// Restore solid fill for the next draw
gd.setRasterizerStateProperty(RasterizerState::CullCounterClockwise);
}
FillMode::WireFrame is not available on the EasyGL renderer when its GL context has no polygon-mode API, which is the case for OPENGLES3 and WEBGL2 (OpenGL ES and WebGL do not expose a polygon fill-mode toggle). The capability stays false there, but EasyGL emulates wireframe for two narrow stock-effect routes, added so unchanged XNA samples run: non-indexed single-stream VertexPositionColor triangle lists (no culling, depth, stencil, scissor, depth bias, MSAA or render target), and indexed 24-byte position-and-normal triangles with clipping, face culling and depth test. Every other wireframe draw is refused. For anything else draw your geometry with PrimitiveType::LineList or LineStrip, or generate wireframe edge geometry explicitly in the vertex buffer.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Device reset, disposal, adapters and format queries — Order and failure boundaries of GraphicsDevice::Reset, the manager's hooks, device-loss status, resource registration and disposal, plus CNA's adapter, format and profile queries.
- GraphicsDevice: the shared device contract — Exact device-level behaviour of CNA's GraphicsDevice: construction and windows, bound state objects, viewport and scissor, Clear and Present, bindings, draw calls, readback and extensions.
- Model, ModelMesh and ModelMeshPart: the runtime graph and its draw contract — What Model::Draw and ModelMesh::Draw do, the invariants a loaded graph supplies, what copies share, the five model content routes and the collection rules, at this snapshot.
- Surface formats: profile gates, renderer verdicts and format usage — How CNA decides whether a texture, cube, volume or render target may use a SurfaceFormat: per-resource profile tables, the renderer verdict, draw-time rules and usage masks.
- Texture data transfer: SetData, GetData, mip levels and streams — Exact SetData and GetData semantics for Texture2D, TextureCube and Texture3D: transfer windows, the CPU shadow, mip levels, compressed blocks, readback and FromStream.
- Vertex and index buffers: CPU shadows, SetDataOptions and layouts — How CNA vertex and index buffers store, upload and read data, what SetDataOptions does on each renderer family, and how vertex layouts reach a renderer by stride and by declaration.
- Vertex declarations, bindings and stream composition — From C++ vertex values to the renderer boundary: stream layouts, VertexDeclaration rules and profile limits, index widths, dynamic updates, VertexBufferBinding, semantic composition, the minimum-offset fold and draw validation order.
- Vertex packing: the glTF stride ABI, index widths and topology — The eleven canonical vertex strides, how a glTF primitive's layout is chosen, typed versus raw upload, index narrowing, the seven topologies, tangents, mirroring and the hard limits the bytes impose.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-246: docs/graphics-resource-lifetime.md says existing XNA resources hold a unique_ptr renderer and names a non-existent ITexture2DRenderer; Texture2D, Texture3D and TextureCube hold shared_ptr renderers and Texture2D's public copies share one — The document's Ownership Model gives unique_ptr renderer ownership for existing XNA resources, a list its scope sentence extends to Texture2D and RenderTarget2D, and knows only one kind of sharing: an internal record on