I need to fix a renderer bug
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Commands, targets, tests, environment variables and symbols were read from CMake, the launchers and the sources at 009d40f5; nothing was configured, built or run, and no GPU, driver or display-server result is claimed.
A renderer bug is hard to place because CNA answers "which renderer?" in several unrelated ways: the identity CMake made the default, the set that was compiled in, the identity the runtime latched, the implementation family behind it, and the host (platform, driver, display server) it ran on. This recipe is the routine for pinning a symptom to one of those axes, reproducing it with the smallest registered test, deciding whether the fault is in shared neutral code or in one translation layer, and proving the fix on a second, genuinely independent backend. Every command, target, test and symbol was read from the CNA source at snapshot 009d40f5; nothing was configured, built or run for this page.
The mechanics are linked, not repeated: Renderer selection, GraphicsDevice, the indexed draw trace, Graphics backends, the generated Selection axes index and the evidence matrix in What to test after changing X.
Find the owner: identity, family, host
Before opening a renderer file, write down the answer to each row; "it is broken on OpenGL" has not yet named an owner.
| Axis | Where it is decided | How to read the answer |
|---|---|---|
| Identity at configure time (default and compiled set) | CNA_GRAPHICS_RENDERER (the default), the optional CNA_GRAPHICS_RENDERERS list and the CNA_RENDERER_<X> switches in cmake/RendererSelection.cmake; names outside the 25 are refused by cmake/RendererIdentities.cmake. | The configure log lines CNA: renderer set -- <list> (default: <X>) and CNA: renderer registry -- <n> family/families, the build tree's CMakeCache.txt and generated/CnaRendererRegistry.generated.cpp. |
| Identity at run time (what the first device got) | CNA::GraphicsRendererSelection in modules/core/src/GraphicsRendererSelection.cpp: an explicit SetPreferred, else the CNA_GRAPHICS_RENDERER environment variable, else the compiled default, plus an optional fallback chain; resolved by GraphicsDevice::resolveRenderer in modules/graphics/src/Xna/GraphicsDevice.cpp. | GetSelected() (never latches), GetActive() (throws before a device exists), IsLatched(), GetFallbackHistory(); on a device GetGraphicsRendererType() and GetGraphicsRendererName(); the startup line CNA: graphics renderer: <NAME> (through the logger to stderr, with (selected at runtime from N compiled in) appended in a multi-renderer build). CNA::getCurrentGraphicsRendererType() is only the compile-time default and can disagree with a multi-renderer device. |
| Family (which directory implements the identity) | The identity-to-family map in cmake/RendererRegistry.cmake. Five GL identities (OPENGLES2, OPENGLES3, OPENGL33, WEBGL1, WEBGL2) are one EasyGL implementation with a run-time profile; OPENGL4 is a separate family; GDI recompiles eight Software translation units with CNA_SOFTWARE_2D_ONLY. | The generated renderer identity table; the family's *RendererDescriptor.cpp under modules/renderers. |
| Host (platform, GPU, driver, display server) | CNA_PLATFORM and the family's descriptor (windowKind, needsGlContext, needsVulkanSurface, needsSurfacePresenter); the machine itself. | The family's startup log (Vulkan: [Vulkan] GPU: <device>; OpenGL4: OpenGL4Renderer initialized with OpenGL <version> on <GL_RENDERER>; EasyGL: a CNA: EasyGL capabilities line), GetRendererCapabilityProfileEXT().GetEnglishReport(), and the display server (Xvfb, a private Xwayland with DRI3, Wine, a real desktop). |
There is no GetDriverName-style accessor at this snapshot. GraphicsAdapter::getDescriptionProperty() is a display name and getDeviceNameProperty() a synthetic \\.\DISPLAY<n> path, so neither identifies a driver; the adapter does carry PCI vendor and device ids. Keep the family's own log lines with the bug report as the driver evidence.
The classification that saves the most time: a bug that reproduces on every family belongs in neutral code (GraphicsDevice validation, state or offset construction, a shared helper); a bug on one family belongs to that family's declaration interpretation, program mapping, submission or resource lifetime. A bug on exactly EasyGL and OpenGL4 may live in the two headers they share, GlStockShaderSources.hpp and GlPresentationSurfaceState.hpp (OPENGL4 and EasyGL).
Symptom to suspect
| Symptom | First suspect | Read first | Start with |
|---|---|---|---|
| Wrong pixels on every renderer | Neutral state or offset construction: GpuDrawParams, element-versus-byte arithmetic, FoldedVertexStreamOffset(), validateDrawState(). | Draw trace: the neutral public method | The neutral fixtures IndexedDrawDeferredTests, OrdinaryDrawBindingOffsetTests and OrdinaryDrawMultiStreamTests in the graphics tests, then a Software_* program. |
| Wrong pixels on one family | That family's declaration or program mapping, sampler or format translation, or present transform (a letterbox or overscan mismatch is a viewport bug, not a shader bug). | The family page from the family map | The family's own named example (Software_IndexedAddressing, Vulkan_DeferredResourceLifetime, EasyGL_RenderTargetPreserveAcrossResize) and a shared *_Parity_* fixture. |
| Crash or black frame after resize | The logical-versus-drawable split: UpdateViewportFromWindow, the renderer's OnSurfaceChanged, then swapchain or framebuffer rebuild. A zero-sized minimised surface must not present. | Present, viewport and the backbuffer, then Vulkan swapchain policy or EasyGL presentation | Vulkan_SwapchainOutOfDate, Vulkan_SwapchainChurn, SdlGpu_SwapchainRecovery, SdlGpu_MinimizedRetry, Software_BackbufferResize. |
| Leak or crash at shutdown | Teardown order: the device drains its resource registry, then renderer, presenter, window and video reference; resources that can outlive a family must be disconnected from it. | Resource registry versus C++ ownership, Ownership and shutdown, Debug shutdown and lifetime | Vulkan_DeviceDisposeOrder, Vulkan_ResourceLeak, Software_ResourceLeak, EasyGL_BoundResourceDispose; Headless_ResourceRenderers for a registry-level leak assertion. |
| Device or context lost | Not one protocol. Six families raise the device events (DirectX 9, 11, 12, Direct2D, WebGPU; Vulkan reports loss once, then throws); the GL families recover per context generation; the rest never call the callback. | Device loss note, EasyGL context loss | Vulkan_DeviceLostContract, EasyGL_Gltf_ContextLoss; the F9 and F10 keys call the renderer's debug context-loss seam from Game::PollEvents. |
| Shader or program compile failure | Decide which of four routes failed: the shared stock GLSL, a custom ShaderEffect, a compiled (MojoShader) effect, or SPIR-V intake. Each takes a different branch of DrawIndexedPrimitivesEx. | EasyGL stock corpus, SDL_gpu shader routes | EasyGL prints Vertex shader compilation failed: style lines to stderr; OpenGL4 reports a failed stock program on its [OpenGL4 GL Error] line; a Vulkan payload problem shows in tools/vulkan/validate_spirv_payloads.py and the SpirvPayloadValidation test. |
| Throw naming Reach or HiDef | Usually correct behaviour. The default profile is Reach, enforced in neutral code as System::NotSupportedException (GraphicsDevice.cpp, IndexBuffer.cpp, OcclusionQuery.cpp, the texture classes): draws above 65,535 primitives, 32-bit user indices, separate alpha blend, non-power-of-two wrap addressing and GetBackBufferData all refuse. | Reach and HiDef profile limits | GraphicsProfileBlendStateTests, GraphicsProfileDrawLimitTests, GraphicsProfileDrawStateFormatTests, GraphicsProfileResourceCeilingTests. A test that died on such a refusal before its assertions is a test defect: tools/platform/profile_dead_tests.py names them. |
| Capability answer wrong | A renderer's SupportsCapability arm that defaults to true, a family virtual left at its false default, or a device-level derivation (seven members are answered at device level: CompiledEffects, HalfFloatTextureLinearFiltering, ComputeShaders, IndirectDraw, both float render-target entries, MultipleRenderTargets). | Capability answers | GraphicsDeviceCapabilityTests, RendererCapabilityProfileTests, GraphicsCapabilityFloatRenderTargetTests; Vulkan is compiled with -Werror=switch so a new enumerator breaks its build until answered. |
Read first
IGraphicsRenderer.hpp: the contract and every default. A default that means "unsupported" is a decision; a default that means "yes" is a candidate bug.- The affected path in
GraphicsDevice.cpp(DrawIndexedPrimitives,SetRenderTargets,Present,Reset,Dispose), so a neutral cause is excluded first. - The family's
*RendererDescriptor.cpp, then the renderer method named by the symptom. - The family's
examples/CMakeLists.txtandTestHelpers.cmake: which named tests exist, under what guard, and which output gates and skip convention decide what a "pass" means.
Reproduce narrowly
Pick the smallest configuration that can show the bug, and record its axes with the result: CNA_PLATFORM, CNA_AUDIO_PLATFORM, the default renderer, the compiled set, the build type and the host.
# a family whose tests register when it is the DEFAULT identity: configure a tree for it
cmake -S . -B cmake-build-vulkan -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DCNA_GRAPHICS_RENDERER=VULKAN -DCNA_BUILD_TESTS=ON -DCNA_BUILD_EXAMPLES=ON
cmake --build cmake-build-vulkan --target cna_test_vulkan_swapchain_outofdate
ctest --test-dir cmake-build-vulkan -N -R '^Vulkan_Swapchain' # what is registered here
tools/platform/run_gpu_tests_private.sh cmake-build-vulkan -R '^Vulkan_SwapchainOutOfDate$' --output-on-failure
# a neutral-layer bug: the unit preset compiles the STUB renderer, so most renderer-gated cases SKIP
cmake --preset unit
cmake --build --preset unit-graphics
./cmake-build-unit/CnaGraphicsTests --gtest_filter='GraphicsProfile*' # run from the repository root
Three facts decide whether that reproduction means anything.
- Which renderer the executable created. The
EasyGL_*andHeadless_*example blocks compare against the build's default identity, so they register only for a default. TheVulkan_*,Software_*,SdlGpu_*,OpenGL4_*andStub_Smokeblocks are guarded byCNA_GRAPHICS_RENDERER STREQUALtheir own identity; the renderers directory re-points that variable while it enters each family, so the block is also entered for a non-default member of a multi-renderer set, and the executables then run under the default renderer. In such a build confirm with the startup line which renderer a test really created before citing its result. - Whether the test ran or skipped. Exit code 77 is a skip (a missing display, adapter or helper), not a pass. Read
ctest -Nand the skip count. Some examples link SDL3 directly and are absent without it (Stub_Smokelinks it unconditionally; the Software and EasyGL example blocks return early without an SDL3 target). - Which display it ran on.
CNA_TEST_DISPLAYis empty by default, so a window test inherits the caller'sDISPLAY. Usetools/platform/run_gpu_tests_private.shfor GPU work: it starts a private headless Weston and a rootful Xwayland with DRI3, exits 77 when either is missing, refuses a build tree whose cache forcesCNA_TEST_DISPLAY, and warns when the private display has no DRI3. Xvfb cannot present Vulkan.
For a selection question rather than a pixel one, the environment is enough. In the multi-renderer preset (HEADLESS;SOFTWARE;STUB, default HEADLESS) CNA_GRAPHICS_RENDERER=SOFTWARE chooses at run time, CNA_DEBUG_UNAVAILABLE_RENDERERS makes a probe fail, CNA_DEBUG_FAIL_RENDERER_INIT makes construction fail and CNA_FORCE_HEADLESS_DEVICE_EXT forces the windowless device mode (device flags).
Pick a second, independent backend
A fix is not proven by the backend it was written against. Choose the second backend by asking which code the suspect shares with it: two backends are independent for a claim only if neither contains the code under suspicion.
| Second backend | What it can separate | Limits and traps |
|---|---|---|
SOFTWARE (internals); also GraphicsAdapter::setUseReferenceDeviceProperty(true) | A neutral-layer error from a native translation error: it rasterises on the CPU inside the draw call, so pixels are readable on any host and there is no queue, fence or device-loss model. | Stock CPU shading only (CustomEffects is false). Its tests link SDL3. A pass is CPU evidence, not hardware parity. GDI shares its sources. |
HEADLESS (internals); also setUseNullDeviceProperty(true) | State, argument and lifetime errors: Validation mode throws a named exception, the resource registry offers AssertNoLeaks(), Trace mode records a call log for drift comparison. | No pixels, and readback is refused on purpose. A shared pixel fixture may pass by asserting the refusal. Do not "fix" a pixel fixture by inventing readback values here. |
STUB (internals) | Only that the neutral route is reachable without a window. | Draws nothing, keeps no bookkeeping, reports every capability false. It is the renderer of the unit preset, so a green unit run is not renderer evidence. |
EasyGL profile (OPENGLES3, OPENGL33) | An immediate-context GL result against an explicit-GPU one; profile differences (ES 1.00 GLSL, base-vertex rebase, MRT limits). | Shares GlStockShaderSources.hpp and GlPresentationSurfaceState.hpp with OPENGL4: agreement between those two does not independently confirm either shared piece. |
OPENGL4 (internals) | A desktop 4.1+ core context with its own loader and window contract; strict [OpenGL4 GL Error] output gate. | Not a sixth EasyGL profile. The 346 EasyGL programs rebuilt against it (OpenGL4_EasyGLParity_*) compare it to EasyGL; they are not a second opinion on the shared headers. |
VULKAN (internals) | A deferred, explicit-GPU replay: catches state read at record time instead of captured at enqueue time, and lifetime bugs behind the fence. | Needs a Vulkan device and a private display with DRI3; the validation output gate fails any registered test that prints [Vulkan Validation]. |
SDL_GPU (internals) | A queued-draw, pass-segment design over Vulkan, Direct3D 12 or Metal chosen by SDL. | Requires the SDL3 platform. One identity over several native APIs: a Vulkan result does not establish Direct3D 12 or Metal. |
Two ready-made comparison layers exist. The 32 renderer-neutral parity fixtures in ParityFixtures.cmake are registered by EasyGL, WebGPU, SDL_GPU and OpenGL4 as <Prefix>_Parity_<fixture> (label Parity); each states its own expected result, so the oracle is the fixture's assertions, not real XNA. scripts/run-parity-fixture.sh also diffs one fixture's raw frame between an EasyGL and a WebGPU build with cna_diag_compare (default tolerance 2); the renderer is a compile-time choice, so two renderers mean two build trees. For "does this match real XNA" use the oracle corpus (tools/xna-oracle/README.md, scripts/run-oracle-corpus-diff.sh, scripts/xna-diff.py) and read Using the XNA oracle as evidence first: the references were captured under Wine and DXVK, and CNA's records show a whole-corpus zero-diff only for DIRECTX9.
Harnesses, output gates and diagnostics
Every renderer example is registered through cna_register_renderer_test in cmake/TestHelpers.cmake; its parameters (ENVIRONMENT, WORKING_DIRECTORY, TIMEOUT, LABELS, SKIP_REGULAR_EXPRESSION) are part of the test's meaning (the Vulkan examples set SDL_VIDEODRIVER=x11 and DISPLAY=${CNA_TEST_DISPLAY}, and the working directory defaults to the top build directory). The output gates that turn diagnostics into failures are attached by that helper and by gtest_discover_tests in UnitTests.cmake:
| Family | Diagnostic hook | What fails a test |
|---|---|---|
| Vulkan | Khronos validation layer in non-NDEBUG builds when installed; CNA_VULKAN_LIFETIME_TRACE=1 (one line per ownership transition); CNA_VULKAN_SAMPLER_TRACE and CNA_VULKAN_TARGET_READBACK_TRACE as narrower probes. | Any output containing [Vulkan Validation], in any configuration that compiles VULKAN; the per-test exemption list is empty. |
| OpenGL4 | A synchronous KHR_debug callback in Debug builds or with CNA_OPENGL4_DEBUG_OUTPUT=1. | Any [OpenGL4 GL Error] line; also empty exemptions. |
| SDL_GPU | SDL's debug mode. | Output matching Validation (Error|Warning), VUID- or a Direct3D 12 ERROR|WARNING line, for every test in its directory. |
| EasyGL | glGetError checks at specific points; no debug-message callback. | Only each test's own assertions. |
| Headless | CNA_HEADLESS_MODE=Fast|Validation|Trace (unset means Validation). | A HeadlessValidationException in the validating modes. |
A message raised while the device is destroyed is out of reach of any in-process assertion, which is why the gates read the process output. Direct3D 11 and 12 have their own debug-layer switches (CNA_D3D11_DEBUG_LAYER, CNA_D3D12_DEBUG_LAYER, CNA_D3D12_GPU_VALIDATION), which need a Windows target.
Sanitizers
Configure the sanitizer in the tree that reproduces the bug instead of assuming a preset covers it. CNA_SANITIZE takes a comma-separated list (for example address,undefined) and CNA_SANITIZE_OPTIMIZATION takes DEFAULT, O0 to O3 (BuildPerformance.cmake); address with thread, and thread with memory, are refused, as are MSVC, non-GNU/Clang compilers, Emscripten and CNA_ENABLE_IPO. The configure line CNA: sanitizers enabled: ... confirms the flag reached the build. The devices-asan, devices-tsan and devices-ubsan presets in CMakePresets.json (Debug, OPENGLES3, CNA_DEVICES=ON, target CnaTests) exist for device work and are not a renderer sanitizer matrix.
For a lifetime bug, reproduce both the explicit Dispose() and the destructor or device-outliving path. Driver-side leaks are recorded, not silenced: lsan_x11_mesa.supp lists only leaks reproduced without any CNA code (including Mesa state that only eglTerminate frees) and deliberately leaves the RADV Vulkan driver's allocation alone; use it through LSAN_OPTIONS=suppressions=tools/platform/lsan_x11_mesa.supp.
Make the smallest change
Put the fix where the wrong decision was made, and keep the invariants that the source tours record.
- Validation belongs in neutral code, translation belongs in the family. A precondition every renderer must hold goes in
GraphicsDevice; layout, resolve and attachment construction stay in the backend. Compare one CPU-staging family and one native family, becauseRequiresManagedBufferedDrawRangeValidationEXT()splits them (EasyGL, OpenGL4 and Software answer false; every other family, Vulkan included, keeps the device's range guard). - Units.
startIndexis an index-element offset;baseVertexand each stream'sVertexOffsetare vertex-element offsets, converted to bytes with that stream's own stride. A byte-offset fix in the neutral layer is almost always the wrong unit. - Deferred families capture at enqueue time. A field that affects replay must be captured when the draw is queued (Vulkan's
Pending3DDraw, SDL_gpu's queued commands); reading it only at record time gives earlier draws the last draw's state. - Capabilities are promises. Answer
SupportsCapabilitywithout a permissivedefault: return true, return false fromEXThooks you do not implement, and refuse 3D work throughEnsure3DSupportedorHandleUnsupported3DCall. Do not advertise a feature whose path throws or silently draws nothing. - Teardown order is contract. A resource class that can outlive its renderer needs a place in the destructor's ordered walk, a release hook and an owner disconnection (Vulkan's live lists, SDL_gpu's
ReleaseStorageBuffersForRendererTeardownEXT, EasyGL's member order with the platform context declared first so it dies last). - The historical pattern. Software once ignored
startIndexandbaseVertexwhile the GPU families were correct; it is fixed and pinned bySoftware_IndexedAddressing. When one family disagrees with the rest, expect the neutral layer to be right and one translation to have ignored a field.
Prove it
- A test that fails before the change, at the narrowest level that can fail: a neutral fixture for a shared rule, the family's named example for a translation bug, a parity fixture when the behaviour is renderer-neutral. Add the second frame, the resize or the disposal step; a one-frame screenshot does not expose stale deferred state or resource retirement.
- The family's tests and its output gate, on a host that has the API, through the private display wrapper.
- The neutral graphics suite (
cmake --build --preset unit-graphics; read its skip count, since the unit preset compiles STUB). - A second independent backend from the table above, running the same source.
- A sanitizer run for anything that changed ownership or teardown.
Write the result the way the evidence supports it: commit, platform, audio and renderer axes, which named tests passed and which were skipped and why, the host and display server, and what was not run. A build is not a test pass, a registered test is not a passing test, a skip is not evidence, and a Wine or software-driver pass is not a native-GPU pass. What STUB, HEADLESS, SOFTWARE and a native renderer can and cannot establish is tabulated in Choose the graphics oracle by the failure you could have caused.
Check the blast radius
| If the change touched | Also check |
|---|---|
GraphicsDevice or a shared header | All 21 families still answer the new default correctly; CPU, GL and explicit-GPU families compile; the C ABI and bindings if a public enum or header moved (Update the C API). |
IGraphicsRenderer contract | An explicit decision in every family, STUB included; the descriptor gate keeps unselected families compiling. |
| A GL shared header | Both EasyGL (per-profile adaptation) and OpenGL4; the profile matrix, not one profile. |
| Descriptor, registry or selection CMake | Change build configuration; single- and multi-renderer configures; scripts/check_renderer_*.py; the combination rules (PORTABLEGL cannot join any real-GL identity, GDI cannot join SOFTWARE, one platform partition per binary). |
| Effects or shaders | Which of the four shader routes changed; compiled effects are default-off on nine families, so a default configure exercises only FNA3D's. |
| Presentation or viewport | Families overriding GetDefaultViewportRect(); letterbox and overscan; a second frame after a resize. |
The broader classification of how far each kind of change reaches is on Blast radius and readiness.
Review checklist
- Does the description name the identity, family and host, and quote the family's startup line?
- Does a test fail without the change, for the right reason and not on a Reach refusal or a skipped branch?
- Was a second backend that does not share the suspect code run, or is its absence stated?
- Are skips, gates and displays reported (private Weston plus Xwayland, Xvfb, Wine, real GPU), with the count of skipped tests?
- Does every family that inherits the changed default still answer truthfully?
- Was state mutated before validation could throw, and does a deferred family capture at enqueue time?
- Is any new native resource in the destructor's ordered walk and disconnected from a device it outlives?
- If a public signature, enum ordinal or capability member moved, was the C ABI baseline considered?
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- CANVAS, HTML_DOM and SVG_DOM: browser object models, refusals and evidence — How CNA's three non-WebGL browser renderers represent sprites, targets and blending, where each refuses work, why two cannot read the back buffer, and which browser tests actually run.
- Capability answers, interface defaults and draw evidence — What a GraphicsCapability answer asks and guarantees in CNA: the 19 contract questions, renderer versus device polarity, known report mismatches, the unsupported-3D policy, four interface-default failure shapes and portable draw claims.
- DIRECT2D and GDI: two Windows 2D delivery stacks — How CNA's two Windows-only 2D renderers draw, refuse, present and recover: Direct2D over a private Direct3D 11 device versus GDI over a private CPU 2D core, and what their tests prove.
- Direct3D evidence: MinGW cross-builds, Wine translators and native Windows — How DIRECTX9, DIRECTX11 and DIRECTX12 are built and tested: MinGW cross-builds, DXVK and vkd3d-proton gates, the shared parity inventory, forced-headless runs and the manual Windows job.
- DIRECTX11 and DIRECTX12 internals: lifetimes, frames in flight, descriptors and target finalisation — Device creation, lifetime groups and recovery, Direct3D 12 frame slots and growable descriptor heaps, HeadlessEXT, readback, MRT and cube finalisation and render-target usage on the DXGI renderers.
- easy-gl and meta-gl: the two-library GL stack beneath the EasyGL family — How meta-gl and easy-gl split loading, typed calls, ownership and failure beneath CNA's five GL identities: revisions CNA needs, feature gating, per-thread state, context loss, tests and build inheritance.
- EasyGL state, clears, targets, queries and buffers: current semantics — What the EasyGL GL-profile renderer does at this snapshot for wireframe, occlusion counts, colour masks, clears, two-sided stencil, fog, base vertex, render targets, context-loss policy and viewports.
- EasyGL: five GL profiles, one implementation, and the other GL-named renderers — What OPENGLES2, OPENGLES3, OPENGL33, WEBGL1 and WEBGL2 share in EasyGL, where they differ, how far evidence carries between them, and how OPENGL4 and PORTABLEGL differ.
- Evidence tiers of the native modern GPU renderers — What VULKAN, SDL_GPU, WEBGPU and METAL implement at this snapshot, what evidence backs each, why a capability bit is not evidence, and the defect shapes these renderers exposed.
- free-direct and free-api beneath the FREEDIRECT renderer — How CNA's 2D-only FREEDIRECT renderer sits on free-direct and free-api: the dependency chain, what each library implements, the fopen macro hazard and its containment, inherited limits and evidence rules.
- Presentation modes, swap interval, native handles and back-buffer readback across renderers — What each renderer family does with the presentation mode, swap interval, formats and full-screen request, how window handles are borrowed, and exactly what GetBackBufferData returns.
- Render targets: usage, cube faces, resolve and readback — RenderTarget2D and RenderTargetCube semantics at the pinned snapshot: construction, binding rules, RenderTargetUsage, cube faces, resolve and mip generation, readback, disposal and lost content.
- Render-target binding, clears, viewport and scissor across renderers — The shared SetRenderTargets transaction, the no-op rebind, what RenderTargetUsage selects, which clears throw, and how each renderer family applies clears, viewport and scissor.
- SDL_GPU shader intake, pipeline keys and draw order — Why CNA's SDL_GPU renderer uses precompiled SPIR-V in SDL_gpu's set convention, which GLSL ShaderEffect accepts, how pipelines are keyed, what state is dynamic, and how draw order and vsync are kept.
- SDL_GPU uploads, render-target lifetime and swapchain recovery — What SDL_gpu validation exposed in CNA's SDL_GPU renderer, when uploads may cycle, how render targets outlive their wrappers, what MRT writes, and how a failed swapchain acquisition keeps the frame.
- SDL_RENDERER: the 2D contract, its refusals and its evidence — Where SDL_RENDERER's 2D boundary sits: execution-time 3D refusal, its single capability, emulated and unhonoured XNA features, address modes, clears, readback coordinates and how its tests run.
- 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.
- The renderer contract: IGraphicsRenderer defaults, factories and failure shapes — Which IGraphicsRenderer bodies a renderer family must write, what each inherited default does to a public call, how null factories fail, and the evidence ladder behind a feature.
- The XNA stock effects: exact semantics, worked uses and verification history — BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect and SkinnedEffect in CNA: defaults, formulas, ordering traps, per-vertex versus per-pixel lighting, worked uses and the defect patterns behind the current code.
- 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.
- Vulkan draw-time state, ordered clears, occlusion queries and descriptor pools — How CNA's deferred VULKAN renderer carries blend, stencil, viewport and scissor state, orders clears, counts occlusion queries and grows descriptor pools, with the defects behind each rule.
- Vulkan presentation, frame pacing and back-buffer readback — How CNA's VULKAN renderer picks its swapchain format and present mode, synchronises two frames in flight, and reads the back buffer without racing the presentation engine.
- WebGPU renderer semantics: surfaces, targets, mips and pipeline state — Exact behaviour of CNA's WEBGPU renderer: non-sRGB surface policy, target-relative SpriteBatch coordinates, cube render targets, blit-free mip generation, dynamic and baked state, ordered clears and BC textures.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-012: Two tests still encode the wrong occlusion-query expectation for SDL_GPU after the renderer was made truthful — SDL_GPU now reports OcclusionQuery false and refuses queries, but the shared capability test still expects true and SdlGpu_OcclusionQuery_Limitation expects a refusal message the public constructor never produces.
- CNA-BUG-099: 24 classic SDL_GPU CTests still fail in CNA's latest recorded run; CNA's records name a cause for only three of them — CNA's SDL_GPU records list 26 classic ^SdlGpu CTests still failing just before TARGET; two are analysed in their own entries. Of the other 24, covering formats, render targets, render state and recovery, the records give
- CNA-BUG-253: SdlGpu_ConstructorExceptionSafety aborts with a double free, a defect CNA's own record calls real and leaves unfixed — CNA's own record says the classic SDL_GPU constructor-failure test aborts with 'free(): double free detected in tcache 2', identically on the unmodified sources, and calls it a real bug to fix later; CNA-BUG-099 quotes t
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Rendering backends: how renderers work · Rendering backends: how renderers are verified · Runtime renderer selection: device flags and debug variables · Reach and HiDef profile limits
- Architecture
- Graphics architecture
- Internals
- Renderer selection internals · GraphicsDevice internals · Indexed draw trace · Graphics backends
- Maintainer workflow
- What to test after changing X · Blast radius and readiness · Using the XNA oracle as evidence · Debug shutdown and lifetime
- Tests and validation
- Test architecture and change recipes · Testing handbook
- Reference
- Selection axes index · Test target index