Repository navigation
Graphics
The SDL3 GPU backend renders through the host graphics pipeline. Render targets are GPU textures, while the GS's swizzled local memory remains their backing store. This permits resolution scaling. The software GS backend remains available as a diagnostic reference.
The shared GS frontend decodes GIF packets and tracks drawing state. Backends consume that state through GSRasterBackend; selecting --gs=sdlgpu or --gs=sw changes the backend, not the game's command stream.
| Location | Responsibility |
|---|---|
src/gfx/backends/sdlgpu/sdlgpu_device.* |
Device, shaders and pipeline state |
sdlgpu_targets.* in the same directory |
Render targets, page ownership, upload and resolve |
sdlgpu_textures.* |
Texture cache, palettes and format expansion |
sdlgpu_backend.*, sdlgpu_display.cpp
|
Draw batching, display composition and presentation |
src/gfx/gs/ |
GS local-memory addressing and transfers |
tools/gsdump/ |
Standalone renderer tests and replay tools |
Shader sources are built with the renderer. macOS translates them to Metal; Apple Silicon has a framebuffer-fetch path for GS blending and related pixel operations.
Draws can make a GPU texture newer than its local-memory backing. Host writes can make local memory newer than a cached texture. A resolve copies GPU results back; a refresh uploads current local-memory data.
Every observation must see preceding draws. Flush queued batches before a transfer or CPU read that depends on them. Invalidate overlapping texture views after writes. GS buffers can alias by physical page even when their base, pitch or pixel format differs, so an address-only texture key is insufficient.
Compatible targets can transfer page ownership or copy feedback directly on the GPU. Unsupported, partial or ambiguous cases retain the resolve path. Never replace a required resolve with stale CPU VRAM merely to remove a readback.
A draw that samples its own colour target reads a snapshot, copied inside the same command buffer just before the batch that needs it. Snapshot textures cycle, so earlier batches keep the copy they sampled, and a draw whose sampled texels miss everything drawn since the last snapshot shares it; the strips of a blur or channel-shuffle pass mostly do. A draw sampling another live target needs no flush when that target owes nothing to local memory, because the draws into it are already ahead in the same submission.
8-bit indexed textures whose pages one CT32 target holds are built on the GPU from that target, at any internal resolution, with the palette from local memory, instead of reading the target back; CT32↔CT16 page reinterpretation does the same. Other indexed textures over GPU-owned pages still resolve first.
Above 1x every target holds each GS pixel as a scale×scale block, and the work that touches a target has to keep that shape rather than fall back to a readback:
- Feedback snapshots that need a padding row from local memory build it at the target's scale. DQ8's shadow channel shuffle reads one row past its view, and padding that worked only at 1x made 2x resolve the target and expand the texture on the CPU for every strip of every frame.
- Host writes into GPU-owned pages upload as CPU patches at any scale; a write that covers whole pages reads nothing back.
- GS local-to-local copies between whole-page CT32 targets of one scale run on the GPU (
gs_local_copy.frag), per sub-sample plane. King Trode's dialogue face animates this way.DQ8_GFX_CPU_LOCAL_COPY=1forces the CPU path. - Bilinear reads of an upscaled target filter across its physical texels, as PCSX2 does, so the frame copy DQ8 makes every frame keeps the extra detail. Point-sampled 2D draws take each texel at the native pixel's sample point, so font sprites whose UVs span one texel more than their pixels do not pull in the neighbouring glyph.
- Sub-sample planes use integer arithmetic:
mod(x, 3.0)on the GPU gave plane 3 for plane 0 at odd scales.
Never read from a mapped upload transfer buffer. SDL creates them write-combined (on Metal, MTLResourceCPUCacheModeWriteCombined), where CPU reads are uncached: building an upscaled row and copying it to the rows below by reading it back cost the GS worker more than the readbacks the patches replaced. Build rows in ordinary memory and only write the mapped buffer.
The GS worker's queue holds 4 MiB of GS work (DQ8_GS_QUEUE_MB changes it). A deeper queue lets the worker keep the GPU a quarter second behind in heavy scenes: the game still renders at full speed, but swapchain images come back late and the window shows a fraction of the frames.
DQ8 shows its frame on both display circuits a line apart and blends them 50/50, a deflicker for interlaced TVs; on a progressive display it is a one-line blur at every internal scale. The menu's Sharper picture setting (on by default in the game) reads both circuits at one origin when they show the same buffer, which leaves the blend a no-op. The renderer itself keeps the blend unless told otherwise, so the replay and test tools stay faithful; DQ8_GFX_REMOVE_LINE_BLEND turns it off for them.
The ordered GS worker owns queued backend operations. Presentation tickets lease immutable snapshots, including their dimensions and GPU fences. Target reuse, reset or later draws must not alter a retained ticket. Window display and guest-frame production remain separate; the tickets do not add interpolation or a configurable presentation clock.
Hardware output is evaluated with focused pixel tests, image differences and visual inspection. It is not universally bit-identical to the software backend, and the software backend itself is not a complete hardware oracle.
GS alpha factors use a divisor of 128 and may exceed one. The Apple Silicon framebuffer-fetch path handles cases that fixed-function blending approximates, including destination alpha, per-bit framebuffer masks and color wrapping. Results on another backend must be checked independently.
Known incomplete areas include alpha-test failure modes other than KEEP, depth/local-memory aliasing, preservation of depth during target growth, and some texture/filtering precision and format reinterpretations. A scene with zero approximation counters does not prove complete rendering accuracy.
Two shortcuts that keep batches large are counted the same way:
-
widened-alpha-replays: a source-alpha blend writes exact alpha with a second, alpha-only draw of the same primitives, after the colour. A strict depth test (GREATER) is widened toGEQUALfor it, so where two primitives of one batch meet at equal depth, or depth writes are masked, a fragment that failed the colour pass can still write alpha.GEQUAL, the usual test, replays exactly. -
interrupted-covered-transfers: a host write skips resolving the GPU pages its rectangle covers whole, since it will replace them. A draw or another transfer arriving before its last pixel sees the part not yet written from local memory, which can be older than the GPU's copy.
- Reproduce the same save, camera and input route; preserve the save before testing.
- Decide whether geometry/GIF data is already wrong or the backend draws correct input incorrectly.
- Use a local GS dump or ordered backend trace to replay the exact workload with software and SDL GPU backends.
- Add a small authored raster/transfer regression for the operation at fault.
- Recheck the live field route after the isolated fix.
Useful optional diagnostics include DQ8_GFX_TRACE_TARGETS, DQ8_GFX_TRACE_BATCHES and DQ8_GFX_DUMP_TEXTURES. DQ8_GFX_DISABLE_FRAMEBUFFER_FETCH=1 selects the older Apple Silicon blend path for comparison; it is not visually equivalent for every GS operation. DQ8_GFX_NATIVE_CAPTURE captures an actual immutable GPU presentation when its trigger file is requested. DQ8_GFX_CHECK_GPU_TEXTURES=1 also builds every GPU-built 8-bit texture the old way and prints how many texels differ; it stalls on each one. Keep captures, checks and tracing outside steady-state FPS measurements.
See Testing for the standalone graphics build. Project Status records the current gameplay limits.
Users
Developers