Need help getting started? Try the player guide. These notes include older work; the game overview explains what’s ready now.

Vulkan hardware scene backend

The optional Windows backend uses Vulkan1.0 graphics pipelines to rasterize textured walls and BSP floor/ceiling triangles with depth testing, plus silhouette-clipped billboard actors with transparent palette-index rejection. It is actual geometry rendering, not an upload of the software-rendered scene. Vulkan is selectable in Options / Graphics alongside Software and GBA when a hardware device and pipeline initialize. play-native.cmd --vulkan overrides the saved choice for a launch.

Vulkan is the default for fresh settings and Clear Data, with automatic Software fallback if initialization or rendering fails. Unavailable Vulkan is hidden from the renderer choices; cycling offers only Software and GBA. Existing saved renderer choices remain intact (including Software for version1 settings). Vulkan uses native projection, vertical-aim policy, FOV and Native/3:2 aspect settings. GBA remains its separate fixed-resolution reference mode. Settings version15 retains the16-byte format, existing renderer values0/1, and all previous choices; byte7 bit7 adds renderer2. Versions1..14 still decode. Invalid renderer3 is rejected.

Optional Graphics effects (2026-10-22)

Graphics offers four independent Vulkan-only toggles, all Off by default: Antialiasing (FXAA), Anisotropic (up to8x), Ambient occlusion (SSAO), and Motion blur. Graphics groups display setup at rows16/24/32/40 (including VSync), filtering and postprocessing at56/64/72/80/88 (Mipmapping, Anisotropic, Antialiasing, Ambient occlusion, Motion blur), and scene details at104/112/120/128. Blank tiles separate the groups; Back remains144. Software/GBA disable and skip these controls while retaining their saved choices. Labels render in small batches in all three languages, and stable action IDs are independent of row order. Settings v22 also stores shared presentation VSync in byte11 bit4; v21 added motion blur in bit3 of the same16-byte CRC record. VSync is independent of renderer selection, defaults On and is disabled when DWM sync is unavailable. Versions1..20 migrate with motion blur Off; v20's other effect choices remain intact. Existing renderer/audio/control preferences are unchanged.

Camera motion blur

The compute pass reconstructs camera velocity from current depth and the previous camera basis/origin, then samples along the resulting screen-space vector. It reads only the current composed scene: no old color frames are blended, preventing persistent frame trails. Depth rejection reduces bleeding across foreground/background boundaries. Sky uses rotational velocity only. This is camera motion blur; independently moving actors have no object-velocity buffer. Translation and yaw/pitch are supported in first and third person.

The effect runs after AO/AA and before fixed-screen actors, weapon, crosshair and HUD composition. Nominal exposure is1/120second, capped to one frame; sampling uses up to12 taps with a24-pixel vector cap. Motion under half a pixel stays exact. Stationary/first frames, pause/death, level changes, reversed frame counters, third-person changes, FOV/size changes, mode toggles, gaps over0.2seconds and large camera cuts reset or bypass blur. The frontend also invalidates history for renderer/blur toggles between scene draws.

Only camera state is retained on the CPU. Motion blur shares the existing GPU-retained surface data and4-byte-per-pixel RGB upload. At most two working image regions alternate between passes: blur reuses the AO region after AA has finished reading it. Compute barriers cover both read-after-write and write-after-read dependencies. Stationary/cut frames omit the blur pass; when no other post effect is active, they also skip the RGB upload, compute submission, fence wait and RGB download. History still advances on those frames. The normal scene pass still retains surface metadata for enabled motion blur. The combined push constants occupy80 bytes (within Vulkan1.0's minimum). Reflections retain their own rendering settings; the composed reflected scene receives the main camera's final blur.

Validation: all eight AA/AF/AO combinations with motion blur, stationary and first-frame exactness, translation blur, pause/level/view/epoch/teleport resets, a real level camera turn, unchanged depth and exact Off restoration. Dispatch counts verify no-op bypass and resumed movement after cuts across all effect combinations. A stationary motion-only frame needs no sample buffer. Vulkan passes8,453,254 checks with zero validation errors and the hidden muted frontend resize check. All11 focused CTests pass. ASan/UBSan audio/settings passes40,058 checks, including save migration and all three localized menus; menu and motion-blurred level captures were visually inspected. No campaign playthrough was run.

The fragment shader performs actual anisotropic footprint sampling of indexed world textures. It derives the principal texel-footprint axes, samples along the major axis up to8 times, selects the existing ROM mip by the minor axis, and samples each tap with nearest-texel lookup. Magnification stays identical to ordinary nearest sampling, with no bilinear edge softening. Minified footprints still average along the compressed axis to retain anisotropic filtering; Above8x it widens the minor footprint. It supports negative/wrapped UVs and applies to reflected world textures too. This is shader filtering of palette-buffer assets, not a samplerAnisotropy switch on nonexistent sampled images. Sprites retain nearest sampling and transparent cutouts; flat-colored planes and CPU surface decals retain their existing shading paths. Disabling mipmaps still uses base textures.

GPU compute postprocessing runs after scene decals/reflections/godrays and before fixed-screen effects, weapons, crosshair and HUD. SSAO reconstructs view-space positions/normals from geometric depth, takes16 bounded nearby samples (48 world-unit radius, max40 screen pixels), and adds subtle contact shading. Sky and billboard pixels are excluded to avoid dark sky or rectangular sprite artifacts. AO now evaluates at the projected center of each base world texel: wall UVs are already texels; floor/ceiling UVs use two world units per texel. A local planar UV chart reconstructs the center in view space before projecting back to the depth buffer. This follows perspective, texture origins and moving surfaces instead of imposing a fixed screen grid. The AO shade is applied without interpolation, retaining visible texel boundaries. Off-screen, occluded or degenerate centers use the current sample to avoid borrowing AO from another surface. This remains screen-space AO, not baked occlusion. Its color pass precedes independently toggleable edge-adaptive FXAA. No gameplay, aim ray, geometry depth or original sprite clipping/priority is modified. All-Off performs no postprocessing allocation/transfer/dispatch. Compute pipelines and device-local working buffers are created lazily and retained. AA alone now reads the color input directly in one dispatch and uploads no unused depth. AO alone uses one output image; combined AO+AA retains two image regions and two dispatches. Buffer ranges track the selected path while allocated capacities persist across toggles. The main-view UV/depth/material attachment is now copied directly into device-local postprocessing storage before reflections reuse the attachments. Normal AO uploads only composed RGB (4 bytes per pixel), eliminating its former16-byte-per-pixel CPU sample array and metadata upload. The existing scene readback for CPU decals/statistics remains. AA alone also uploads only4 bytes per pixel. See the2026-10-22 revision in technical-refactors.md for parity checks and timings.

The existing hybrid renderer requires one extra upload/readback when either post effect is On. Uploads use host-coherent staging; compute inputs/working colors stay in device-local memory. Transfer->compute, compute->compute, compute->transfer and transfer->host barriers plus the existing fence protect all passes. The surface block is16-byte aligned, including odd target sizes. Retained metadata belongs to one renderer and its dimensions, is invalidated when that target starts another draw, and is consumed by postprocessing. A reflection target can reuse attachments without replacing the main metadata. An explicit owned host-sample override supports standalone postprocessing and differential tests; ordinary scene rendering allocates no such array. Sky and sprite exclusion now occurs in the shader, with identical masked depths. See the Khronos synchronization examples. Resizes, 1-pixel/odd targets, repeated toggles, shared reflection resources and teardown are covered by the Vulkan validation checks. Only the main view needs the post passes; reflected texture filtering occurs in each reflected draw.

Validation includes synthetic flat/sky preservation, diagonal-edge smoothing, local depth-contact shading, elongated-footprint texture detail, all7 feature combinations, exact Off restoration and unchanged Software/depth outputs. Settings roundtrips/migration and keyboard/mouse disabled-row navigation pass. Menu captures were inspected in all three languages. The hardware run on an RTX4060 Laptop GPU passes8,453,163 checks with zero validation errors, plus native frontend launch/resize sanity; no campaign playthrough was run. New checks prove exact nearest-sampling magnification, retained anisotropic minification detail, constant AO within world texels on wall/flat charts, slanted perspective and shifted negative UVs. All11 focused CTests pass. The combined-effect pool capture was visually inspected. Retained-GPU versus masked-host metadata produces exact output across reflection attachment resizes, target growth/shrink and AO with/without AA.

A short8-frame-per-scene1920x1080 diagnostic measured all three enabled against Off: level-start8.26->13.60ms; pool11.51->16.30ms; reflected pool25.50->30.85ms. Heavier shadow/godray/crowd scenes added roughly11..12ms. These are isolated render timings, not full-game FPS guarantees. See vulkan-effects-performance.json. All three remain optional because their cost depends on scene and GPU.

Ownership and rendering

SsaPcHardware is a borrowed scene interface, independent of Vulkan and Win32. The shared scene code prepares near-clipped geometry and current actor frames. The GPU consumes batched triangles, current palettes and column-major indexed textures. A reversed D32 depth buffer handles occlusion. Two offscreen color attachments return RGB and visible material/UV/depth information. Nearest texture sampling retains the pixel art; hardware float interpolation and edge coverage can differ slightly from the double-precision software rasterizer.

The scene's existing exact CPU decal/water rules shade the visible plane samples. Reflections perform a second GPU geometry pass using the mirrored/clipped camera, then the existing half-alpha water composition. The GPU snapshot finishes before any reflection reuses its resources. Transparent actors exclude covered water pixels from the reflection mask. Fixed-screen effects, HUD, menus, NETRICSA, fades and final GDI window presentation retain their existing composition. Readback and CPU effects remain synchronization/performance costs: this is a hybrid hardware renderer, not a completely GPU-resident frame. The performance correction and measured limits are described below.

The frontend owns one Vulkan context across level changes. Scene and reflection renderers borrow it; their destruction never destroys the device. Resource growth and target resize happen only after the previous fence completes. Transfers, render-pass transitions and host reads use explicit Vulkan barriers. Offscreen targets do not require a window swapchain and are unaffected by minimize or F11; the existing presentation path handles windows and letterboxing.

vulkan-1.dll is loaded at runtime: PCs without Vulkan can still launch the software/GBA build. Initialization rejects CPU devices, missing graphics queues, unsupported formats/features and failed allocations. A runtime render failure detaches both scene backends, releases the device and redraws in software with a notice. GPU limits are checked before target allocation. Shader binaries are compiled into the library; the released executable needs the GPU driver, not the SDK or loose shader files.

Build and checks

Stable sprite priority (2026-10-21)

The recovered GBA queue paints actors far-to-near, with later submissions winning equal-depth ties. Software and Vulkan now share stable billboard-plane ordering; source order is retained in groups spanning at most0.001 units. Sorting uses camera-forward XY distance before pitch, and each secondary layer follows its base layer. Reflections use the same ordering.

Actor triangles are clipped on the CPU to the original GBA wall/portal silhouettes before submission. World triangles populate reversed depth first; the second pipeline paints these prepared sprites without testing or writing depth. This avoids slicing corpse/explosion artwork at the floor, preserves wall occlusion and transparent-index discard, and prevents sprite depth fights. There is no longer a sprite-only depth bias. Geometric depth returned to CPU effects is unchanged. World and actor batches normally require two draw calls. See sprite-clipping.md for original rules and regressions.

The regression suite covers18 sprite-priority cases, floor-crossing artwork, solid wall hiding, pitch/FOV, reflections, late-submitted world primitives and unbiased returned depth. The latest RTX4060 run passes6,487,023 checks and28 paired scene draws with zero validation errors. All ten focused CTests and the hidden180-frame Vulkan launch/resize check pass.

Install the Vulkan SDK with glslangValidator for a Vulkan-enabled Windows build. CMake discovers it through VULKAN_SDK; without it, or with -DSSA_VULKAN=OFF, the optional backend compiles as an unavailable stub. No ROM assets are embedded in shaders or binaries. Core still links ssa_pc_scene, never the HUD compositor or platform Vulkan library.

Run scripts/test-vulkan.ps1 -Validation to build and check on a hardware GPU with the Khronos validation layer. Add -SkipBuild to reuse the build. It checks depth occlusion, transparent texels, odd-size and1-pixel targets, resizing, software pixel comparisons, level/third-person views, FOV extremes, actual pool reflection, renderer switching, menu availability and settings controls. It does not run a campaign playthrough. Results and input hashes are recorded in vulkan-validation.json; snapshots and validation logs are under build/native-windows/test-output/vulkan.

The2026-10-20 run on an NVIDIA GeForce RTX4060 Laptop GPU passed4,868,499 checks over22 paired draws with zero validation errors. Compared images matched 99.13..100% of pixels (4,858,486 of4,868,374 overall), including third person, FOV60/120, decals and the Slave Compound pool. A hidden, muted180-frame frontend smoke run completed using Vulkan. Eight focused CTests passed. No campaign playthrough or subjective control/audio validation was performed.

Synchronization guidance: Khronos synchronization examples and rendering submission lifecycle.

Performance correction, 2026-10-20

The2026-10-21 follow-up combines depth/statistics/effects processing into one parallel row pass, removes redundant hardware buffer clears, and copies repeated sky rows within each draw. Three paired benchmark runs show21..30% lower median isolated frame time with identical captured pixel/depth/water hashes. See technical-refactors.md for the current measurements, validation and ownership details. The older timings below remain the historical result of the first stall fix.

The first implementation selected the first coherent host-visible memory type for both uploads and downloads. On the RTX4060 Laptop GPU, reading individual sample components from that uncached mapping caused enormous CPU stalls: 1080p isolated draws took617..2110ms (0.47..1.62fps). Coherent does not mean cached. See the Khronos memory allocation rules.

Uploads and readback now use separate persistent buffers. Readback prefers HOST_CACHED|HOST_COHERENT, then cached non-coherent memory with explicit invalidation after the transfer barrier and fence. If neither cached type is available, a bulk copy into ordinary CPU memory precedes sample processing. Non-coherent readback is never written by the CPU. Buffer capacities grow geometrically; descriptor ranges still cover only actual data. The GPU stores zero output alpha directly instead of requiring a CPU mask over every pixel.

Independent RGB decal/water rows run synchronously on a bounded Windows worker pool (at most seven workers plus the render thread, scaled down on smaller machines). Jobs only write disjoint pixels; statistics remain on the caller. All jobs finish before reflection reuse, another frame, or device destruction. The indexed GBA reflection path remains serial because it updates a shared palette cache. The portable scene library still depends on no Windows APIs.

Reflections retain full native resolution and projection, but rasterize, upload, download and shade only rows consumed by visible water. One-to-one reflection composition skips redundant bilinear taps. Camera/palette setup runs once per water height, after rejecting already-seen stencil pixels. No settings, decal patterns, water blending or simulation cadence changed.

Run scripts/bench-vulkan.ps1 -SkipBuild for four isolated1080p scenes,60 frames each, with wall-clock transfer/wait/effect timings. -Software measures the reference path; -Width, -Height and -Frames vary the workload. Validation layers are disabled only for benchmarking. Reports include source hashes in performance-vulkan.json. These are render timings, not a promise of end-to-end gameplay FPS: audio, simulation and presentation are excluded. Full-resolution reflected water is still the most expensive case.

Final60-frame local measurements on the RTX4060 Laptop GPU at1920x1080:

Scene Effects Milliseconds/frame Render FPS
Level1 start Decals off 11.710 85.40
Level1 start Decals on 16.558 60.39
Slave Compound pool Decals, no reflections 18.849 53.05
Slave Compound pool Decals and reflections 31.467 31.78

The separate before/after comparison uses identical three-frame camera workloads:616.973/711.717/1271.430/2109.613ms before versus12.599/14.686/17.790/30.653ms after, respectively. Normal timing variation and longer camera sweeps explain differences from the60-frame table.

Regression coverage additionally checks partial-row transfers leaving outside rows intact and exact serial/parallel RGB, depth and water-mask equality over eight animation phases at an odd target size. The ordinary Vulkan/software image comparisons, resize/startup smoke check and portable renderer tests remain. Final checks:4,869,177 Vulkan assertions with zero validation errors; exact serial/parallel output equality; eight focused CTests;11,169,555 portable renderer assertions under ASan/UBSan with leak detection; hidden muted180-frame Vulkan frontend startup/resize check. The Windows playable executable was rebuilt.