SDL_gpu renderer internals
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. Driver-specific live validation remains open: no workflow selects SDL_GPU, Direct3D 12 evidence is a Wine/vkd3d-proton route, and Metal has none recorded here.
modules/renderers/sdl-gpu/ implements the one public SDL_GPU renderer identity. It is neither an OpenGL wrapper nor CNA's SDL platform backend: it owns an SDL_GPUDevice, records every CNA draw in CPU-side command queues, and only acquires a GPU command buffer and a swapchain texture when the frame is flushed. That delay is the key to its resource lifetime, render-target ordering and debugging rules. At this snapshot the family also carries a SPIR-V shader intake, a 3D custom-effect path, compute, storage buffers and images, indirect draws, instanced stock draws, shadow reception and image-based lighting; this page covers the whole of it for a maintainer who has to change or debug it.
Where platform, renderer and native driver meet
CMake SDL_GPU identity -> cna_renderer_sdl_gpu (REQUIRES_PLATFORM SDL3, links SDL3::SDL3)
-> SdlGpuRendererDescriptor: RendererWindowKind::Plain, window + video subsystem
-> GraphicsDevice -> SdlGpu::CreateGraphicsRenderer(args)
resolves the SDL_Window from args.surface.windowId (the family's one SDL3 edge)
-> SDL_CreateGPUDevice(SPIR-V [+ SDL_shadercross formats], debug mode, driver auto-selection)
-> SDL_ClaimWindowForGPUDevice(device, SDL_Window*) (skipped in headless mode)
-> SDL_gpu native driver: Vulkan / Direct3D 12 / Metal, whichever SDL chose
sdl-gpu/CMakeLists.txt creates the target with cna_add_renderer() and sets REQUIRES_PLATFORM SDL3. There is no separate SDL_gpu library: SDL_gpu.h is part of the vendored SDL3 every SDL-backed renderer links, so an X11, Wayland or Win32 platform selection is not sufficient merely because it can supply a window, and the SDL2 platform and CNA_ENABLE_SDL=OFF refuse the identity by name (see Translation and adapter layers). The family links SDL3::SDL3 after every static archive that calls it (SDLGPU-98: single-pass GNU/MinGW archive resolution otherwise left SDL_shadercross's SDL references unresolved).
SdlGpuRendererDescriptor.cpp declares RendererWindowKind::Plain, needsWindow and needsVideoSubsystem true and AlwaysAvailable: the descriptor does not probe the host, so a registered SDL_GPU identity is no promise that a device will be created. It needs a plain window, not an OpenGL context. The factory at the end of SdlGpuRenderer.cpp resolves the SDL_Window* through CNA::Platform::Detail::ResolveSdl3RendererWindow(args.surface.windowId) and forwards virtual size, presentation mode, swap interval, multisample count and depth format; everything else comes from SDL. Review renderer selection internals before changing registration.
| Build switch | Default | What it controls |
|---|---|---|
CNA_SDL_GPU_SHADERCROSS | ON on Windows and Apple, OFF elsewhere | Declared in RendererSelection.cmake; includes ThirdPartySDLShaderCross.cmake (pinned SDL_shadercross plus SPIRV-Cross), links cna_sdl_shadercross statically and defines CNA_SDL_GPU_SHADERCROSS. A configure with the option on and no configured target is a FATAL_ERROR. |
CNA_SDL_GPU_COMPILED_EFFECTS | OFF | Compiled XNA effects through MojoShader's own SDL_GPU adapter; links cna_renderer_mojoshader_effect. See Compiled effects. |
GLSL ShaderEffect route | detected, no option | The family CMake looks for a target-native shaderc_shared/shaderc library, but never on WIN32, APPLE or EMSCRIPTEN targets, and never by globbing host library directories when cross-compiling. Found: it links the library, defines CNA_SDL_GPU_SHADER_EFFECTS and sets CNA_SDL_GPU_SHADER_EFFECTS_AVAILABLE; the configure log says which. |
CNA_SDLGPU_TEST_VIDEO_DRIVER | x11 on Linux; windows, cocoa, android, uikit elsewhere | Cache variable in examples/CMakeLists.txt: the SDL video driver the integration tests run under (see validation). |
Device and presentation setup
The public constructor delegates to a test-hook constructor with an empty SdlGpuTestHooksEXT. Every native acquisition happens inside a local ConstructionResources object, which owns the partially created device, window claim, construction shaders, ShaderCross session and window-registry entry until the last fallible step has succeeded; only CommitTo() moves raw handles into renderer members. The order in the constructor body is:
- Test-headless guard:
CNA_SDLGPU_TEST_FORCE_HEADLESSis honoured only when it is exactly1, and with a real window it is refused (std::runtime_error) unlessSDL_GetCurrentVideoDriver()isdummyoroffscreen. Headless mode is a null window (the publicPresentationParameters::HeadlessEXTroute) or that test switch. - Debug mode: on whenever
NDEBUGis not defined, which on Vulkan asks SDL for its validation layer. - Shader formats: SPIR-V, plus
SDL_ShaderCross_GetSPIRVShaderFormats()after a reference-counted ShaderCross session is acquired when the option is built. Without ShaderCross the constructor first asksSDL_GPUSupportsShaderFormatsand throws "no installed SDL_gpu driver accepts SPIR-V; rebuild with CNA_SDL_GPU_SHADERCROSS=ON" when nothing does. SDL_CreateGPUDevice, thenSDL_ClaimWindowForGPUDeviceunless headless.- Swapchain:
ConfigureSwapchainandSDL_GetGPUSwapchainTextureFormat; in headless mode an RGBA8 colour-target format, falling back to BGRA8, or a refusal. - Depth/stencil format for the requested
DepthFormat, and the sample count clamped byClampSampleCountagainst the chosen formats. CreateConstructionShaders: the stock sprite, colored, textured, lit, alpha-test, dual-texture, environment-map, skinned and PBR vertex/fragment modules (instanced variants are created later, on first use).- Drawable pixels from
SDL_GetWindowSizeInPixels(virtual size in headless mode), thenIGraphicsRenderer::RegisterForWindow. CommitTo(*this),BeginBackbufferSegment()(the backbuffer owns a real first bind cycle), and anSDL_Logline naming the backend SDL actually chose (SMG-0031).
If any step throws, ~ConstructionResources unwinds in reverse: window registry entry, construction shaders (reverse order), window claim, device, ShaderCross session. SdlGpuFailurePointEXT names every stage a test can fail on purpose (DeviceCreation, WindowClaim, SwapchainSetup, DepthStencilFormatQuery, each construction shader, WindowMetricsInitialization, RendererRegistration, AfterRendererRegistration, and later FrameCommandBufferAcquisition, GraphicsPipelineCreation, SamplerCreation and the default-texture creations); successful stages still acquire real SDL objects, and SdlGpuTestHooksEXT::resourceEvent reports every acquisition and release (device, window claim, shader, frame command buffer, pipeline, sampler, default texture, ShaderCross session, storage buffer) so a test can prove that rollback releases exactly what was acquired. forceNoDepthStencilFormat and forceShaderCrossCompilation force the two branches a Vulkan host would otherwise never take.
Swap interval: SDL_gpu has no half-rate present mode, so PresentInterval.Two becomes VSYNC. Interval 0 prefers IMMEDIATE, then MAILBOX (recorded as applied interval 1, because mailbox is still vblank-synchronised); a refused SDL_SetGPUSwapchainParameters is logged and leaves the previous applied value. In headless mode the interval is only recorded. GetDriverNameEXT() returns SDL_GetGPUDeviceDriver: record it in every bug report, because the public identity says nothing about which native API ran.
Headless here is a renderer mode that renders into an offscreen proxy instead of a swapchain. It is not evidence that every SDL_gpu driver works without a display, and a headless pass says nothing about window claim, swapchain acquisition or presentation.
Shader routes: stock SPIR-V, SPIR-V intake, GLSL and compiled effects
Five different code paths create shaders on this renderer. A shader bug starts with deciding which one is involved.
| Route | Input | Where | Availability |
|---|---|---|---|
| Stock effects and SpriteBatch | Committed SPIR-V in spirv_shaders.hpp | CreateSdlGpuShaderEXT, route chosen by SelectSdlGpuShaderCreationRouteEXT | Every build with a device that accepts SPIR-V, or whose formats SDL_shadercross can produce |
| Instanced stock vertex modules | The same sources compiled again with CNA_INSTANCED | StockVertexShaderEXT, created on first use | As the stock route |
ShaderEffect from SPIR-V | Caller or shader-package SPIR-V words (LooksLikeSpirvEXT) | SdlGpuEffectRenderer::CompileSpirvProgramEXT after RemapSpirvForSdlGpuEXT | Every build (no runtime compiler needed) |
ShaderEffect from GLSL | GLSL text compiled to SPIR-V by libshaderc at run time | SdlGpuEffectRenderer::CompileProgram | Only with CNA_SDL_GPU_SHADER_EFFECTS; otherwise an invalid effect with a compile error naming the missing compiler |
| Compute | SPIR-V only | SdlGpuComputeShaderRenderer::CompileProgram | Any device (see modern resources) |
| Compiled XNA effects | D3D9 Effect Framework bytecode via MojoShader | SdlGpuCompiledEffect.cpp | Only with CNA_SDL_GPU_COMPILED_EFFECTS |
Stock shaders. The GLSL sources under src/shaders are compiled offline by compile_shaders.py (libshaderc through ctypes; not run by CMake) into the checked-in header; never edit the header apart from its sources. The script now does two textual things before compiling: it inlines #include "shadow_sampling.glsl" from the Vulkan renderer's shadow_sampling.glsl (SMG-0032: one piece of shadow GLSL, with CNA_SHADOW_* defines moving its resources into SDL_gpu's sets), and it inserts an instance prologue into every vertex shader that is the identity unless CNA_INSTANCED is defined, in which case four per-instance matrix columns arrive at locations 12 to 15 (STREETS-0001). The old position-only instanced3d module is gone. SelectSdlGpuShaderCreationRouteEXT picks DirectSpirv when the device accepts SPIR-V, ShaderCross when the device's formats intersect what SDL_shadercross can produce (or when the test hook forces it), and Unsupported otherwise; the ShaderCross route reflects the SPIR-V and refuses a module whose reflected sampler, storage and uniform counts differ from the create info. The shader sources follow SDL_gpu's set convention, not the Vulkan renderer's, as the script's docstring warns.
SPIR-V intake (SMG-0006/0007). CNA's portable shader packages are compiled from sources written for the Vulkan renderer's descriptor layout. SdlGpuSpirvBindings.hpp is the one translation layer: RemapSpirvForSdlGpuEXT rewrites a module word by word into the layout SDL_gpu mandates, reports each resource's original and assigned (set, binding) and its SDL slot, and turns a push_constant block into a uniform buffer (SDL_gpu has no push constants). A block that is not already std140-legal is refused by name, and a failed remap must be surfaced, never replaced by the untransformed module, because a module left in the Vulkan layout binds the wrong resources silently.
| Stage | set 0 | set 1 | set 2 | set 3 |
|---|---|---|---|---|
| vertex | sampled textures, storage textures, storage buffers | uniform buffers | — | — |
| fragment | — | — | sampled textures, storage textures, storage buffers | uniform buffers |
| compute | sampled textures, read-only storage textures and buffers | read-write storage textures and buffers | uniform buffers | — |
The portable conventions the runtime binds against are constants in SdlGpuRenderer.hpp: an effect's own textures in set 1 at bindings 0 to 3 (2D), 4 to 7 (cube) and 8 to 11 (volume), up to kMaxCustomEffectSamplersEXT = 4 units per kind; the FloatArray, Vec2Array, Vec3Array and Mat4Array uniform blocks at bindings 12 to 15 (72 std140 elements each, held in a shared SdlGpuUniformArrayBlockEXT); and six engine-owned matrices at binding 19 (kEngineMatricesBindingEXT). The texture unit is read from the declared binding, not from the compacted SDL slot, because a sampler the compiler dead-strips shifts every later slot.
The SPIR-V intake and the compute pipeline hand SPIR-V straight to SDL_CreateGPUShader/SDL_CreateGPUComputePipeline; neither goes through SDL_shadercross, whereas SupportsShaderLanguageEXT(SpirV, …) and SupportsComputeShadersEXT() answer true for any device. On a Direct3D 12 or Metal driver the create call is therefore the point of refusal. This is a reading of the source at this snapshot; no D3D12 or Metal run was made for this page.
Why one public draw is not yet one GPU draw
DrawPrimitivesEx and DrawIndexedPrimitivesEx first send a compiled-effect draw to QueueCompiledEffectDraw (when built), then call DispatchStockDrawEXT. That function now starts with the custom-effect case: a valid SdlGpuEffectRenderer goes to QueueCustomEffect3DDrawEXT (SMG-0019; before that, a 3D ShaderEffect draw fell through and was shaded by whatever stock family its declaration matched). Otherwise CollectStockVertexStreamsEXT and SelectStockVertexShapeEXT choose a shape and one typed queue: colored, textured, colored-textured, lit, alpha-test, dual-texture or environment-map, with StrideDerived reserved for SkinnedEffect (selected by effect state) and the PBR strides; anything else throws NotSupportedException. SdlGpuVertexBufferRenderer and SdlGpuIndexBufferRenderer keep CPU shadow copies because user draws may pass temporary sources that are gone before Present.
Each queued command snapshots what the draw needs by value: matrices (with the XNA pixel-centre correction), topology, vertex and index bytes, effect uniforms, RenderStateSnapshot (blend equation, per-slot colour write masks, cull, wireframe, both depth-bias values, stencil state and reference), sampler slots, and textures as SdlGpuSampledTextureEXT values whose keepAlive keeps the owning GPU state alive until the submit that consumes it. PushDrawOrder, the one choke point, appends a QueuedDrawRef carrying the queue kind, index, pass segment, viewport, scissor and blend factor. Replaying "all sprites, then all 3D", or reading live state at Present, would change public draw order and state semantics. Newer snapshots follow the same rule:
- Custom 3D effects. The attribute location of a declaration element is its index in the
VertexDeclaration(the convention EasyGL and Vulkan use); reflection drops elements the shader does not read and a consumed location the declaration does not supply throwsNotSupportedException. The command captures the 128-byte per-draw block (overlaid byApply3DUniformsEXT), uniform arrays, reflected uniform/sampler/storage maps, bound textures, theSamplerStates[0..3]that XNA applies per texture unit (STREETS-0004), storage buffers published throughBindStorageBufferForDrawEXTwith keep-alives (SMG-0020), and an instance count. Its pipeline key adds stride and every attribute's location, format and offset. - Instanced stock draws (STREETS-0001).
DrawInstancedPrimitivesExroutes a compiled effect and a custom effect with their instance count; a custom effect that is not a valid SDL_GPU effect throwsNotSupportedException. Without a per-instance stream it is an ordinary indexed draw. Otherwise it range-checks every per-instance and per-vertex binding (ArgumentOutOfRangeException) and re-entersDispatchStockDrawEXTwithpendingInstanceCountEXT_set, so the effect family is kept and only the vertex module changes. Skinned and PBR draws, whose vertex record is fixed, carry the matrix columns in a second per-instance buffer at slot 1 (InstanceTransformStreamEXT). - Indirect draws (SMG-0023/0025).
DrawPrimitivesIndirectEXTandDrawIndexedPrimitivesIndirectEXTaccept only this renderer's own storage buffer (NotSupportedExceptionotherwise), park the buffer, offset and ashared_from_this()keep-alive inpendingIndirect*members for the duration of the dispatch, and clear them on every exit. At replay the arguments stay on the GPU: CNA'sIndirectDrawIndexedArgumentsis field-for-fieldSDL_GPUIndexedIndirectDrawCommand. - Shadow reception (SMG-0032).
CaptureShadowReceptionEXTfills the 132-floatCnaShadowParamsblock float-for-float as Vulkan and WebGPU do, resolves the directional or cascade map, the point-light cube and the spot map with keep-alives, and copiesSamplerStates[7..9]. A shadow map attached after the draw is not that draw's. Receivers always bind all three samplers, so neutral default textures (including a white cube) are created on the queueing side, where a failure still has a caller.
At flush, EnsureFrameRendered acquires one SDL_GPUCommandBuffer (owned by a FrameCommandBufferOwner), inserts the frame's queued SetStringMarkerEXT labels (SMG-0027), and acquires one swapchain texture with SDL_WaitAndAcquireGPUSwapchainTexture, or uses the proxy in headless mode. A null texture (for example a minimised window) is a non-error skip, but a command buffer that attempted acquisition is still submitted, on every exit. Sprite and scene vertex/index data is uploaded in a copy pass before any render pass, because SDL_gpu forbids a copy pass inside a render pass. The walk over passSegments_ then opens one native pass per segment in bind order and calls RenderQueuedDraws in the captured order, the readable backbuffer proxy is blitted to the swapchain if active, and the buffer is submitted; SDL presents the acquired swapchain texture on submission, so Present() only calls EnsureFrameRendered. The same flush also runs from ReadBackbuffer, render-target readback and FlushPendingGpuWorkEXT (compute dispatch, storage-buffer readback and copy). The neutral trace leading here is in the indexed draw trace.
Modern resources: compute, storage buffers and images
SdlGpuModern.hpp and SdlGpuModern.cpp hold the CNAEXT engine layer's resources (SMG-0012): SdlGpuStorageBufferRenderer, SdlGpuStorageTexture2DRenderer and SdlGpuComputeShaderRenderer. The ordering rule this deferred renderer needs is stated there: a dispatch first flushes the pending frame (FlushPendingGpuWorkEXT) and then records its own command buffer, so SDL's submission order yields render, compute, render and compute-write-before-graphics-read without hand-written barriers.
- Storage buffers. One
SDL_GPUBufferplus a CPU shadow, zero-filled on creation. SDL_gpu has no uniform-buffer usage at all, so aConstant-usage buffer is pushed from its shadow withSDL_PushGPU*UniformDataat bind time.GetDataRangeEXT,CopyToEXTandRefreshShadowFromGpuEXTflush the pending frame first, so a readback observes work queued before it. Every buffer registers with the renderer (RegisterStorageBufferEXT); see destruction order for why. - Storage textures. SDL_gpu has no separate storage-image object: a texture is one when created with the compute storage usage bits.
TranslateStorageImageFormatEXTis the single format table, shared withGetSurfaceFormatUsageSupportEXT, which asksSDL_GPUTextureSupportsFormatseparately for storage read, storage write, sampled and render-target usage. - Compute shaders.
CompileProgramaccepts SPIR-V only, remaps it, refuses a non-compute entry point and refuses integer samplers (CNA textures are float-sampled), then creates the pipeline with the reflected counts andLocalSize.DispatchEXTbinds storage buffers and textures by reflected slot, leaving a gap for an unbound resource instead of shifting neighbours (SMG-0013), gives every declared sampler a real image (default white texture and a nearest/clamp compute sampler), and pushes the uniform block or a bound constant buffer. Named scalars needOpMemberName, which an optimised package lacks.
| Reported limit | Value while a device exists |
|---|---|
| Work-group count per axis | 65535 |
| Work-group size X / Y / Z; invocations | 128 / 128 / 64; 128 |
| Storage / uniform buffer bytes | 134217728 / 16384 |
| Compute storage-buffer bindings; sampled textures per stage | 8; 16 |
| Storage / uniform offset alignment | 256 / 256 |
These are Vulkan 1.0 minimum guarantees hard-coded in SdlGpuRenderer.cpp, because SDL_gpu exposes no device limits; they are not measured values. Capability answers come from several places: the renderer's own SupportsCapability switch answers ComputeShaders, IndirectDraw, the two float render-target members and OcclusionQuery false, while GraphicsDevice derives the public answers from SupportsComputeShadersEXT(), SupportsIndirectDrawEXT() and the format probes (true whenever a device exists), so the device-level answer is the one to trust (capability matrix). SupportsBaseInstanceDrawingEXT, SupportsShadowSamplingEXT and SupportsImageBasedLightingEXT (the PBR shader's split-sum term, STREETS-0002) are true; HalfFloatTextureLinearFiltering is true when HalfVector4 is a supported render-target format (STREETS-0003); CreateOcclusionQuery throws NotSupportedException because the vendored SDL_gpu exposes no query commands. Use-side coverage is in modern features by renderer.
Render targets are ordered pass segments
A PassSegment is one public bind cycle: exactly one native SDL_BeginGPURenderPass/SDL_EndGPURenderPass pair. Every bind opens a new segment, including a rebind of the same target, face, mip or MRT set, and segments are appended in the order they were opened, so replay is public order by construction (REMED-GFX-145). A target drawn into, unbound, sampled by a later draw and rebound in one frame is three passes, not two; merging by target identity would lose order and load actions. The backbuffer has real segments too (REMED-GFX-143): kSwapchainSegment (0) survives only as the before-first-cycle sentinel, empty earlier backbuffer segments issue no pass, and the last backbuffer segment always gets one so the acquired swapchain image is left presentable.
- A
Clear()belongs to the segment it was issued in. If a draw of that segment already precedes it,SegmentForOrderedClearcloses the segment and opens another over the same destination (openedByClear, REMED-GFX-156); leading clears fold into the pass load action. - Load and store actions belong to the segment: an explicit clear wins, then the resource's first-use clear flag, otherwise
LOAD. An MSAA target resolves into its sampled texture; when a later segment loads the same resource (SegmentColorLoadedLater) or contents are preserved, the store op becomesRESOLVE_AND_STORE. Backbuffer depth is always stored so a later backbuffer cycle still depth-tests against it. - MRT extras (
rts[1..]) are attachments of the segment their primary bind opened, with every attachment's own format in the pipeline description; a cube face in a multi-target set throws. A single cube face binds through its own route. - Mip chains of a rendered target are regenerated per segment by
GenerateRenderTargetMipChain, after the pass ends (REMED-GFX-187).
The renderer does not treat a public target wrapper as the owner of GPU state. SdlGpuRenderTarget2DState, SdlGpuRenderTargetCubeState and SdlGpuSampledTextureState hold the native handles, are kept alive by shared_ptr from segments and commands, and in their destructors queue textures through QueueTextureRelease instead of releasing them. After a successful submit the renderer drains pending texture and pipeline releases, and SDL_gpu's own fencing takes it from there. On a flush exception the command buffer is finished for failure, transient draw buffers are released with ReleaseSceneDrawBuffers(false), and the queued logical frame is kept, so a retry can re-record it; a resource fix must examine both paths. The neutral side of target lifetime is in textures and render targets.
Resize, readback and limitations
OnSurfaceChanged throws std::invalid_argument for a renderer without a window and for a different window identity; otherwise it records the drawable extent and a sanitised display scale (non-finite or non-positive becomes 1). Backbuffer depth and MSAA textures are sized when the next frame acquires its actual swapchain extent, so a minimised zero-size frame does not render. ComputeLogicalViewport applies the presentation modes (native, fixed-height dynamic-width, stretch, letterbox, overscan). Once ReadBackbuffer is called, backbufferReadbackEnabled_ makes a self-owned proxy with the swapchain's format the draw destination (the swapchain texture is write-only) and the proxy is blitted to the swapchain before submit; nothing is paid until the first read. The read flushes, clamps to the proxy, downloads through a transfer buffer and a fence, swaps channels for a BGRA swapchain, and zero-fills anything outside the proxy or nothing at all if no proxy exists (a minimised window). If a mismatch appears only after resize or a screenshot, inspect proxy activation, the pass target format and sample count and the copy order before changing GraphicsDevice.
Explicit limits at this snapshot:
OcclusionQueryis refused (see above);GetAdditionalLimitationsTextEXT()says so.- That same text still states that CNAEXT
ShaderEffectinstancing is not implemented, althoughDrawInstancedPrimitivesExnow queues a custom-effect draw with its instance count (SMG-0022, the particle route that reads per-instance data from a storage buffer bygl_InstanceIndex). What the custom-effect command does not do is bind a per-instance vertex stream: it binds vertex buffer slot 0 only. Treat the text as stale and the stream limit as real. - The GLSL
ShaderEffectroute exists only where the build found libshaderc, andCustomEffectsfollows that route alone (see renderer availability). - One identity sits over several native APIs: a Vulkan result does not establish Direct3D 12 or Metal behaviour. Headless stock-draw validation (
ValidateStockDrawForDriverEXT) is not native presentation.
Destruction order is part of the renderer contract
~SdlGpuRenderer runs these steps in this order, and each one depends on the next object still being alive:
- Release the cached compute sampler while the device exists.
- Unregister the window mapping.
- Drop queued scene, sprite, segment and order data (
ReleaseSceneDrawBuffers,spriteCommands_,passSegments_,drawOrder_). Releasing a command's last texture reference may enqueue a native handle; the drain below frees it. - Drop the storage-buffer draw bindings and the pending indirect-argument keep-alive (SMG-0040: as members they would otherwise die after the device, when a buffer can no longer release its
VkBuffer). - Destroy stock resources family by family: PBR, skinned, instanced, environment map, dual texture, alpha test, lit, textured, colored, sprite.
- With compiled effects: release compiled-effect pipelines, then every native compiled effect through
ReleaseCompiledEffectsForRendererTeardownEXT(a public effect may outlive its device and becomes native-state-free), then the program leases, then the MojoShader context. - Drain
pendingTextureReleases_andpendingGraphicsPipelineReleases_; release depth, backbuffer MSAA and readback proxy textures. ReleaseStorageBuffersForRendererTeardownEXT: every storage buffer still alive, whoever holds it, releases its native buffer and is detached, so its later destructor touches neither SDL nor the renderer.- Unclaim the window, destroy the device, and finally release the ShaderCross session.
Reversing command-drop and device destruction would leave delayed releases addressing a dead device; reversing compiled-effect pipelines and the MojoShader context risks callbacks into a destroyed context. A new native resource class needs the same treatment: a place in this walk, and detachment if its public object can outlive the renderer. See debugging shutdown and lifetime and architectural invariants.
Validation that matches the risk
examples/CMakeLists.txt registers the integration tests inside if(CNA_BUILD_TESTS AND CNA_GRAPHICS_RENDERER STREQUAL "SDL_GPU"). modules/renderers/CMakeLists.txt re-points CNA_GRAPHICS_RENDERER to each family's identity while it enters that family, so this block is also entered when SDL_GPU is a non-default member of CNA_GRAPHICS_RENDERERS; cna_register_renderer_test sets no renderer selection, so in such a build confirm which renderer an executable actually creates (the EasyGL and HEADLESS blocks compare against _cna_default_renderer_identity for exactly this reason). The environment comes from CNA_SDLGPU_TEST_VIDEO_DRIVER: only X11 receives CNA_TEST_DISPLAY, dummy/offscreen clear both display variables, and every test in the directory fails on Validation (Error|Warning), VUID- or D3D12 (ERROR|WARNING): output (SDLGPU-115).
- The shared parity corpus from
ParityFixtures.cmakeasSdlGpu_Parity_*, plusSdlGpu_Parity_backbuffer_msaa_Reset(the A 4x, B 0x, A 4x reset mode). - Construction and lifecycle:
SdlGpu_ConstructorExceptionSafety,SdlGpu_Smoke(now also checks the capability answers),SdlGpu_SamplerCacheLifetime,SdlGpu_SwapchainRecovery,SdlGpu_MinimizedRetry,SdlGpu_PresentationSurface,SdlGpu_PresentLifecycle. - Targets and order:
SdlGpu_RenderTarget2D,SdlGpu_MRT,SdlGpu_RenderTarget2DMSAA,SdlGpu_DrawOrder,SdlGpu_GraphicsDevice_OrderedClear,SdlGpu_Backbuffer_PassOrder,SdlGpu_RenderTarget_PassBoundary,SdlGpu_BoundTargetLifetime,SdlGpu_DeferredSourceLifetime. - No-swapchain routes:
SdlGpu_HeadlessGraphicsDevice(publicHeadlessEXTdevice;SDL_GPU_DRIVER=vulkan, ordirect3d12on Windows) andSdlGpu_2D_HeadlessDriver(the unchanged 2D scene withCNA_SDLGPU_TEST_FORCE_HEADLESS=1). - Added for the modern work:
SdlGpu_StorageBufferLifetime(noSDL_GPUBuffersurvives device destruction in any order),SdlGpu_TextureDefragment(forces SDL_gpu's Vulkan defragmenter over textures),SdlGpu_InstancedPbr3D,SdlGpu_InstancedStockFamilies,SdlGpu_BasicEffect_DiffuseColorClamp,SdlGpu_EnvironmentMapEffect_AmountClamp,SdlGpu_ShaderEffect_PerUnitSampler(Vulkan's witness compiled again without its validation leg) and, only withCNA_CNAEXT,SdlGpu_HalfFloatFiltering. - Built but deliberately not registered:
cna_test_sdlgpu_modern_stress, a 3000-cycle soak over storage buffers and textures, texture arrays, resized offscreen targets, indirect draws, a custom-effect sprite and shadow reception, checking resident memory, file descriptors and threads (exit 77 without a modern route); and the manual diagnosticcna_diag_sdlgpu_single_sprite. - GoogleTest sources under
tests/: compiled-effect, sampler-cache and ShaderCross tests; the compiled-effect one is also built standalone asSdlGpu_CompiledEffectRuntimewhenCNA_SDL_GPU_COMPILED_EFFECTSis on.
Cross-compiled Windows executables run through run-wine-vkd3d-headless.sh (fresh Xvfb, SDL dummy, direct3d12, forced headless, a vkd3d-proton engagement gate); that is Wine evidence, not native Windows presentation, and no workflow selects SDL_GPU. On a non-Windows host the headless registrations reuse the ordinary test environment, which selects x11 by default, while the constructor accepts the forced-headless switch only under dummy or offscreen; check CNA_SDLGPU_TEST_VIDEO_DRIVER before reading a SdlGpu_2D_HeadlessDriver result. None of these tests was executed for this page.
A workable order: ctest --test-dir <build> -N -R SdlGpu to see what is registered, then the targeted subset, then -L SdlGpu or the parity label; run_gpu_tests_private.sh is CNA's private-compositor runner for GPU tests (see Tutorial 131). For a change, decide first whether it touches enqueue-time capture, replay-time state, pass segmentation, SDL resource ownership or a shader route, and add a test whose sequence separates old from new behaviour: target A, backbuffer, A again; a texture or storage buffer destroyed before Present; a resize between acquisition and rendering; a dispatch followed by a readback. Run it on the native driver named in the report and on a second driver where possible. If an interface signature changes, check GraphicsDevice internals and the other renderers too.
Read in this order
sdl-gpu/CMakeLists.txt, the SDL_GPU block ofRendererSelection.cmakeandSdlGpuRendererDescriptor.cpp: the SDL3 dependency, the three shader switches and the pre-construction contract.SdlGpuRenderer.hpp:PassSegment,QueuedDrawRef, the typed draw commands, the native state wrappers, the binding constants and the failure points, before the implementation.SdlGpuRenderer.cpp:ConstructionResourcesand the constructor, thenDispatchStockDrawEXTand oneQueue*Draw,EnsureFrameRendered,RenderToTarget, and the destructor.SdlGpuSpirvBindings.hppwithSdlGpuSpirvBindings.cpp, thenSdlGpuModern.hppwithSdlGpuModern.cpp: the SPIR-V intake and the modern resources.compile_shaders.pyandexamples/CMakeLists.txt: how shader artifacts are produced and what a named test actually runs.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- 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.
- 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.
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-094: SDL_GPU claims compute and SPIR-V shader intake on every device, including Direct3D 12 and Metal drivers that cannot take raw SPIR-V — SupportsComputeShadersEXT() and SupportsShaderLanguageEXT(SpirV) answer true whenever a device exists, but compute pipelines and ShaderEffect SPIR-V payloads reach SDL_gpu as raw SPIR-V, which its Direct3D 12 and Metal d
- 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-203: SdlGpu_2D_HeadlessDriver forces headless mode under the x11 video driver that the SDL_GPU constructor refuses for it — On non-Windows hosts the SDL_GPU headless registrations reuse the ordinary test environment (SDL_VIDEODRIVER=x11 by default), but SdlGpu_2D_HeadlessDriver also sets CNA_SDLGPU_TEST_FORCE_HEADLESS=1, which the constructor
- 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
- CNA-GAP-008: SDL_GPU ShaderEffect draws bind only the first vertex stream, and the limitation report misstates ShaderEffect instancing — SDL_GPU's custom-effect draw takes its attributes from the first vertex buffer and binds slot 0 only, so other streams are never consumed, while its limitation text calls ShaderEffect instancing unimplemented although in
- CNA-VGAP-025: No CI workflow selects WEBGPU or SDL_GPU; WebGPU evidence is local Linux and scripted headless Chrome, and SDL_GPU's Direct3D 12 and Metal driver routes have no native execution evidence — Neither identity appears in any workflow; WEBGPU's native evidence is Linux x86_64 only, and SDL_GPU's Direct3D 12 route has only Wine/vkd3d-proton runs while its Metal route has none.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Modern GPU APIs · Translation and adapter layers · Tutorial 131: SDL_GPU · Shader effects: renderer availability
- Architecture
- Graphics architecture
- Internals
- Renderer selection internals · Indexed draw trace · Textures and render targets · Vulkan (shares the shadow GLSL) · SDL3 platform internals
- Maintainer workflow
- Fix a renderer bug · Debug shutdown and lifetime · Architectural invariants
- Tests and validation
- Test architecture · What to test after changing X
- Reference
- Test target index · CMake option index