Parse, translate and compile libretro slang shader presets (.slangp /
.slang) as an embeddable C++23 library — pure-C binding included — with a
Vulkan execution path.
.slangp ──parser──▶ pass chain ──translator──▶ per-stage GLSL ──glslang──▶ SPIR-V ──runner──▶ Vulkan
The core layer is extracted from the proven preset stack of VTSTech/DevilutionX's GPU shader mod (corpus-validated against all 2,658 libretro presets; provenance and divergences documented in PROVENANCE.md). The format contract is docs/SPEC.md; the project plan with milestones, API sketch and risk register is docs/PLAN.md.
Status: M4.06 — async rendering and device-lost recovery. All library-side M0–M4
gates are green: 29/29 ctest, byte-exact goldens, zero validation-layer
errors on lavapipe. The M0 core suite is joined by the M1 runner skeleton
(Device adopt/create, offscreen Chain::render, golden blit round-trips), the
M2 execution contract (history rings OriginalHistory#, feedback
double-buffering PassFeedback# / <name>Feedback, per-pass FBO formats,
mipmap_input refill, FBO recreation on resize, live parameters), M3 LUT
uploads, and — since M4 — swapchain presentation: the final pass draws into an
acquired swapchain image with the letterbox rect and orientation handled by
the library (plan §8 output mode (a)). M4.05 adds the pure-C API
(include/slangvk/c.h — C API) and the
single-submit per-frame upload path (Device::createMappedBuffer +
InputFrame::pixelBuffer). M4.06 splits the frame path into record / submit
/ wait, so a host can keep a frame in flight
(Chain::render_async() + Chain::wait_async()), and adds device-lost
recovery (Device::recover(), with the preset's textures and the chain
rebuilt automatically).
Per-item implementation status, audited against the plan: docs/CHECKLIST.md;
notable changes per milestone: docs/CHANGELOG.md.
Corpus verdict (M4.03): the full libretro preset corpus — all 2,658 presets, pinned to the revision recorded in the ledger header — is swept through the real load path, and the resulting per-preset baseline (corpus/baseline-ledger.txt) is committed and gated: 2,637 loaded / 21 failed, zero regressions since the 2026-10-17 baseline (826 / 1832). The remaining 21 are upstream-broken or malformed presets (parameter-only overlays, stray-quote keys, missing LUT declarations) plus one intentionally-invalid fixture — not library failures; each is enumerated in the changelog.
DevilutionX integration (plan §11 M4) is underway on the fork's
libslang-vk
branch: gpu_scaler keeps its public interface and runs the library's
Vulkan runner dual-path with the SDL_GPU runner — preset browser, ini
persistence, OSD overlay and the (Unavailable) fallback continue unchanged
on top (plan §9). Hardware field sweeps on Windows + Linux are the spike's
exit gate.
- Quickstart
- Shader demo
- Consuming
- Vulkan runner
- C API
- Example
- Conformance: golden images + perf budgets
- Building and testing
- CI artifacts
- Documentation
- License
Build and test — glslang, Vulkan-Headers, volk and VMA are fetched and SHA-256-pinned automatically, so no Vulkan SDK is needed at link time:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failureThen load, render and switch real shader presets with the bundled demo:
./build/examples/shader_demo # menu over the bundled presets
./build/examples/shader_demo path/to/slang-shaders # browse any preset pack
./build/examples/shader_demo --no-menu --out shots # headless: PNG per preset, exitAnd embed the library in four calls:
auto device = slv::Device::create({.appName = "myhost"});
auto preset = slv::Preset::load(*device, "shaders/crt-royale.slangp");
auto chain = slv::Chain::create(*device, *preset, {.outputWidth = 1280, .outputHeight = 720});
chain->render(inputFrame, outputTarget, {}); // once per frameThe same runner speaks C too — include/slangvk/c.h
is an extern "C" mirror of the C++ surface (see C API).
render() waits for the frame to retire. A host that would rather keep a
frame in flight can split it:
auto fence = chain->render_async(inputFrame, outputTarget, {}); // returns immediately
// ... the host's own work for this frame ...
chain->wait_async(*fence); // before the output is usedAt most one async frame may be in flight per chain — the frame's texture bindings live in the chain's shared descriptor sets — but the host still gets real CPU/GPU overlap between the two calls.
If the Vulkan device is lost (driver crash, GPU reset), the chain rebuilds it,
the preset's textures and itself, and reports slv::Errc::vk_device_lost:
the host's own images died with the device too, so recreate them and render
again. Hosts can also drive the recovery themselves with
Device::recover().
examples/shader_demo is a working host built
directly on the public API — the end-to-end embedding story. It discovers
presets, renders chains and switches presets, input sources and shader
parameters live, in two forms:
- Windowed (default when GLFW is buildable): opens a Vulkan window and plays the M4 presentation contract from the host side — the demo owns the window, surface and swapchain and the acquire/present pair, while the library draws the chain into the acquired image with fit-and-center letterboxing.
- Headless (automatic fallback when no display exists, or
--headless): renders warmup frames offscreen and writes the final frame as a PNG — how the ctest smoke runs on CI/lavapipe.
shader_demo # menu over the bundled presets
shader_demo ~/slang-shaders # browse a full shader pack
shader_demo crt/blend.slangp # load one preset directly
shader_demo --no-menu --frames 2 --out shots pack/ # dump one PNG per preset, exit
shader_demo --list # print what would be offeredMenu commands (terminal-driven, both modes):
| Command | Action |
|---|---|
<number> |
load + switch to preset number N |
n / p |
next / previous preset |
i |
toggle input: testcard.png (default) ↔ animated pattern |
w |
write PNG (headless: render + dump now; windowed: screenshot) |
l / list |
list presets around the current one |
params |
show the current preset's shader parameters (live values) |
set NAME VAL |
override a parameter (applies on the next frame) |
r / reload |
re-read the current preset from disk and rebuild its chain |
q |
quit |
Notes:
- The default input is the bundled
testcard.pngtest card — your sprites show up at startup. The card is found next to the executable first (so CI artifact zips work anywhere), then via the build-time assets path and the working directory.--input pattern/iswitches to the animated test pattern, whose frames go throughImage::updateevery frame — the M4 per-frame host-upload path a game host uses for its converted frame. - Preset switching re-points the SEC-01 shader-root sandbox per source, so a single menu can mix the bundled demos, the test fixture trees and a full slang-shaders checkout.
--validateturns onVK_LAYER_KHRONOS_validationwhen installed.
CMake options:
SLANGVK_DEMO_WINDOW(defaultON) — fetches GLFW 3.4 and compiles the windowed path;OFFgives a pure headless demo build with no GLFW fetch. GLFW 3.4 hard-fails its configure when a Linux windowing stack is only half present, so every required dev package is probed up front and GLFW is only fetched when one full backend exists: X11 needs thelibx11-dev,libxrandr-dev,libxinerama-dev,libxext-dev,libxcursor-dev,libxi-devheaders, Wayland needslibwayland-dev(withwayland-scanner) pluslibxkbcommon-dev. With neither stack installed (typical CI images), the demo silently builds headless-only instead of failing configure. First-configure overrides:-DGLFW_BUILD_X11=ON/OFF,-DGLFW_BUILD_WAYLAND=ON/OFF.SLANGVK_DEMO_FETCH_SHADERS(defaultOFF) — shallow-clones libretro/slang-shaders at configure time and seeds the demo's default preset search with it (large download; runtime paths work without it too).
include(FetchContent)
FetchContent_Declare(libslang-vk
GIT_REPOSITORY https://github.com/VTSTech/libslang-vk.git
GIT_TAG main)
FetchContent_MakeAvailable(libslang-vk)
target_link_libraries(myapp PRIVATE slangvk::slangvk) # static
# or slangvk::shared for the shared build#include <slangvk/slangvk.hpp>
slv::preset::SetShaderRootDir("shaders"); // once, at startup
slv::preset::Preset preset;
std::string error;
if (slv::preset::ParsePreset(slv::preset::ResolvePresetPath("crt-royale.slangp"),
preset, error)) {
for (const auto &pass : preset.passes) {
slv::preset::SlangTranslation t;
slv::preset::TranslateSlang(slv::preset::ResolvePresetFilePath(preset.dir, pass.shaderPath),
{}, /*passIndex*/ 0, /*passCount*/ 1, t);
slv::preset::PassSpirv spirv;
slv::preset::CompilePass(t, spirv, error);
// spirv.vertex / spirv.fragment: SPIR-V for the Vulkan runner
}
}The descriptor conventions the translated GLSL targets (vertex UBOs set 1, fragment textures set 2, fragment UBOs set 3) are exactly what the Vulkan runner consumes; a host rolling its own pipeline can use them too.
Hosts rolling their own translate loop should also seed the translation's
texture universe with #pragma name pass names before translating: presets
that tap a pass's output as <name> (this frame) or <name>Feedback (the
previous frame) fail with unknown texture reference otherwise.
slv::preset::CollectSlangPragmaNames(shaderPath, names) collects them with
the same include semantics as TranslateSlang; Preset::load does this
automatically.
The runner executes compiled presets on a Vulkan device. The host either
adopts its own context or goes headless; errors come back as
std::expected<T, slv::Error>:
#include <slangvk/slangvk.hpp>
// Headless device (lavapipe/CI) or slv::Device::adopt(instance, pd, dev, queue, family)
auto device = slv::Device::create({.appName = "myhost"});
auto preset = slv::Preset::load(*device, "shaders/crt-royale.slangp");
slv::ChainOptions opts{.outputWidth = 1280, .outputHeight = 720};
auto chain = slv::Chain::create(*device, *preset, opts);
// One frame: input texture in SHADER_READ_ONLY_OPTIMAL, output into your view.
// .image fields are needed for history/mipmap presets and feedback-read
// final passes (the runner blits from/into them; content is never modified).
chain->render(
{/* .view = */ inputView, /* .image = */ inputImage, VK_FORMAT_R8G8B8A8_UNORM, 640, 480,
slv::Rotation::None},
slv::Offscreen{outputView, outputImage, VK_FORMAT_R8G8B8A8_UNORM, 1280, 720},
{/* .frameCount = */ 0, 16666.7F, 1});M1+M2 semantics already live: multi-pass chains with per-pass scale rules
and aliases, the pinned UBO fill (sizes, FrameCount mod, parameters —
alias/#pragma name sizes resolve to the owning pass), history rings
(depth+1 RGBA8 slots, seeded warmup, T-N indexing per SPEC 5.2),
demand-driven feedback double-buffering with a final-pass resolve blit
(SPEC 5.3), per-pass target formats, mip-chain refill for mipmap_input
readers (F1), FBO recreation on resize, and case-insensitive live
parameters.
M3 adds lookup textures (SPEC 3.5): image files decode to RGBA8 and
upload as R8G8B8A8_UNORM with the LUT's own filter/wrap options and a
full mip chain generated at upload when name_mipmap = true. Bindings
resolve by declared name (case-insensitive, F9) or positionally via
User# (first-declared order), <LutName>Size uniforms carry the
uploaded size, and git-lfs pointer files fail the load with an explicit
diagnostic. PresetLoadOptions::noLutImages keeps compile-only sweeps
(no device work); chains then fail at bind time with a named error. A
Preset must outlive the Chains created from it (LUT views are
borrowed). M4 adds presentation: render into an acquired swapchain image
with SwapchainFrame (the host acquires and presents, the library waits
the acquire semaphore and leaves the image in PRESENT_SRC_KHR), with
fit-and-center or explicit-rect letterboxing (reference-host present
math, linear sampling, black margins), input-frame rotation (the chain
renders in source orientation, every Rotation uniform carries the
value, the present draw rotates the output), and Image::update for
per-frame host uploads. Final passes that do not resolve to the output
target size letterbox through the same draw instead of failing. Gates:
golden blit + full chain semantics + LUT goldens + letterbox/rotation
goldens + a full acquire/render/present/readback cycle on
VK_EXT_headless_surface — headless on lavapipe with a live
validation-error gate — tests/test_runner_headless.cpp (M1),
tests/test_runner_m2.cpp (M2 field canaries), tests/test_runner_m3.cpp
(M3) and tests/test_runner_m4.cpp (M4).
M4.05 trims the per-frame sync cost for hosts that upload every frame:
Device::createMappedBuffer() hands back a persistently mapped staging
buffer — memcpy into it and pass it as InputFrame::pixelBuffer, and the
chain records the upload copy at the top of its own frame submit
(shader-read → transfer-dst → copy → shader-read barriers, before history
blits and passes run). One command buffer, one submit, one fence wait per
frame — the standalone Image::update round-trip's second submit + fence
drain disappears, along with the per-frame staging alloc/free.
Image::update stays for headless and test use.
Vulkan is loaded at runtime through vendored volk (no link-time libvulkan); memory comes from vendored VMA. Hosts adopting a context keep owning their instance/device/queue.
Since M4.05 the runner also ships a pure-C surface,
include/slangvk/c.h: stddef.h/stdint.h +
vulkan/vulkan.h only, extern "C", consumable from C and any FFI
language. It mirrors the C++ API 1:1 — opaque slangvk_device /
slangvk_image / slangvk_preset / slangvk_chain handles,
slangvk_result codes matching slv::Errc one-for-one, and thread-local
slangvk_last_error() carrying the last failed call's message. Device
creation mirrors the two C++ modes: slangvk_device_create (library-owned)
or slangvk_device_adopt (host-owned instance/device/queue).
/* abridged — the header's opening comment is the full walkthrough */
slangvk_device dev; slangvk_image img;
slangvk_preset preset; slangvk_chain chain;
slangvk_device_create(&dev_opt, &dev); /* or slangvk_device_adopt(...) */
slangvk_image_create(dev, &img_opt, &img); /* input texture */
slangvk_preset_load(dev, "shaders/crt/royale.slangp", NULL, &preset);
slangvk_chain_create(dev, preset, SWAP_W, SWAP_H, swap_format, &chain);
/* per frame: */
slangvk_image_update(img, my_cpu_framebuffer, GAME_W * GAME_H * 4);
slangvk_image_get_handles(img, &in.view, &in.image, &in.format, &in.width, &in.height);
if (slangvk_chain_render_swapchain(chain, &in, &out, &fi) != SLANGVK_OK)
fprintf(stderr, "render failed: %s\n", slangvk_last_error());Contracts carry over verbatim (input layout, preset outlives chains,
single-threaded chains, resize before render after an extent change;
destroy order chains → presets → images → device). slangvk.hpp pulls the
header in and both slangvk and slangvk-shared build the adapter, so the
CMake consuming story is unchanged.
The C surface mirrors the C++ one, including the device-lost path:
slangvk_device_recover() rebuilds a lost device (and
SLANGVK_ERR_DEVICE_LOST_RECOVERY_FAILED reports a failed recovery).
tests/test_c_api.cpp consumes only the C header
and drives the full loop — device create, image create/update, preset load
- parameter metadata + live override, identity/gain chains, resize, error
paths — with byte-exact readback expectations (ctest:
c_api), skipping cleanly when no Vulkan ICD is present.
examples/preset_info.cpp is a complete
end-to-end demo in ~180 lines: it loads a .slangp, prints the pass chain,
scale rules, aliases and parameters, then translates and compiles every pass
to SPIR-V with glslang — printing the per-stage bytecode sizes and the dense
texture bindings the Vulkan runner creates. Two demo presets are
bundled under examples/presets/: a single-pass
brightness shader (demo-bright) and a two-pass glow chain exercising
per-pass scale rules, linear filtering and pass aliases (demo-glow).
cmake --build build --target preset_info
./build/examples/preset_info # bundled demo-bright
./build/examples/preset_info examples/presets/demo-glow.slangp
./build/examples/preset_info path/to/any.slangp # any preset on diskEach demo preset also runs as a ctest smoke test (example_preset_info_*),
so the example path is covered by CI. Exit code is 0 only when every pass
parses, translates and compiles — convenient for checking foreign presets.
examples/corpus_sweep.cpp is the M3
conformance harness — and since M4.02 the baseline-ledger generator: it
walks preset directories through the real load path and reports a per-preset
ledger (parse/translate/compile/LUT upload), optionally executing chains on
the Vulkan device:
cmake --build build --target corpus_sweep
./build/examples/corpus_sweep path/to/shader-pack # compile-only sweep
./build/examples/corpus_sweep pack --render --lut # + render 2 frames per chain
./build/examples/corpus_sweep pack --fail-on-broken # nonzero exit on failures
./build/examples/corpus_sweep pack --ledger new.txt --baseline corpus/baseline-ledger.txt
./build/examples/corpus_sweep pack --only-from list.txt # rescan only the listed presets--baseline diffs the fresh ledger against a pinned one and fails the run on
LOAD→FAIL regressions (improvements are reported as review material);
--corpus-rev stamps the shader-pack revision into the ledger header;
--only-from restricts the sweep to the newline-separated preset paths in
FILE while keeping corpus-root semantics — the chunked-rescan escape hatch
for slow machines. This is the tool behind
corpus/baseline-ledger.txt (2,658 presets:
2,637 loaded / 21 failed at the pinned revision) and the nightly gate.
--render executes every chain through the real render path — presets
whose final pass does not resolve to the output size go through the M4
letterbox present draw instead of failing — and fails the run on
any validation-layer error. The bundled-fixture smoke runs as a ctest
(example_corpus_sweep_smoke); the full sweep runs in CI nightly — see
CI artifacts.
Sample output (demo-glow, trimmed):
pass 0: shaders/demo_downsample.slang
filter linear, wrap clamp-to-edge, mipmap off
scale x: x0.5 source, y: x0.5 source
alias 'blur', frame_count_mod 0, format rgba8
...
pass 1 shaders/demo_glow_combine.slang: OK
vertex SPIR-V 1172 bytes (293 words), fragment SPIR-V 1428 bytes (357 words)
set 2 textures: Original=0 blur=1
parameters:
GLOW = 0.600 (default 0.500, range 0.000..2.000 step 0.050) [preset]
result: 2/2 pass(es) compiled to SPIR-V — ready for the Vulkan runner.
The two M3 leftovers ship as one tool, conformance — golden-image
rendering for visual regressions and per-frame wall-clock budgets for
performance regressions. Both modes skip cleanly (exit 0) when no Vulkan
device is available, like corpus_sweep.
Golden images render every preset listed in
tests/golden/manifest.txt — 21 bundled
fixtures covering every chain semantic (blit, multi-pass, feedback
ping-pong, history ring, mipmap refill, FBO formats including the
upstream R32_UINT integer pack/unpack round-trip, push-fragment
uniforms, parameters, size uniforms, smoothness qualifiers, named / User#
LUTs, LUT mipmaps, LUT size, rotation, include composition, the two demo
presets) — from a deterministic procedural testcard (320×240) at a fixed
target (640×480, --size WxH), 3 frames per chain, and compare the final
frame against the committed PNG in tests/golden/images/. A pixel passes
when every channel differs by ≤ max-delta (default 2); the image passes
when the mismatched-pixel fraction is ≤ max-mismatch-pct (default 0.5%)
— same backend + driver is byte-exact in practice, the slack absorbs
cross-version LSB drift. The manifest's last header line,
# generated-by:, records the backend that produced the goldens; a
different local device prints a WARN once (cross-backend drift on real
GPUs is expected — regenerate with --update when adopting a new
reference backend). The suite's one non-default tolerance belongs to
presets/demo-glow.slangp (200 / 12.0): its exactly-0.5× downsample
puts every box tap on a source texel boundary, where linear filtering
rounds differently per backend (measured llvmpipe vs Radeon RX 560:
worst 156 on 8.7% of pixels, byte-exact within each backend). First
real-GPU validation, RX 560 2026-10-19: 19/20 against the llvmpipe
goldens (feedback worst 2, mip worst 3, the rest ≤ 2) and 20/20
byte-exact on-device after --update — see PROVENANCE Session 7.
cmake --build build --target conformance
./build/examples/conformance golden tests/data examples # compare (ctest: example_conformance_golden)
./build/examples/conformance golden tests/data examples --update # (re)generate goldens + stampPerf budgets load every preset under the roots (--only-from FILE
whitelist to restrict), render --warmup (default 3) + --timed
(default 20) frames and time each frame — Chain::render is synchronous
(one fenced submit per frame), so wall-clock around it is the honest
per-frame cost including submit + execute + fence wait. --report FILE
writes a TAB ledger (mean/min/max ms, fps, pass count) suitable for
committing and diffing across driver/hardware changes; --budgets FILE
gates the mean — path<TAB>max-ms lines, breach exits 1, presets absent
from the budgets file are informational only, budget entries whose preset
was not measured are tolerated (the corpus-ledger gate philosophy).
tests/golden/budgets-lavapipe.txt
is the format reference (5× measured + 5 ms floor, sandbox-informational);
for your own GPU run --report there and set budgets from the measured
means — 16.6 ms is the 60 fps ceiling, mean × 1.5 a pure regression guard.
A committed real-GPU reference ships too:
tests/golden/perf-rx560.txt (Radeon RX 560
Series, Windows — every fixture 0.29–1.20 ms mean, 835–3,457 fps at
640×480) with the derived regression-guard budgets
tests/golden/budgets-rx560.txt
(max(1.5 × mean, 1.0 ms) — the floor absorbs cross-run jitter on the
sub-0.5 ms fixtures; regenerate from a fresh --report after driver
changes).
./build/examples/conformance perf tests/data examples --report my-gpu.txt
./build/examples/conformance perf tests/data examples --budgets tests/golden/budgets-lavapipe.txt
./build/examples/conformance perf tests/data examples --budgets tests/golden/budgets-rx560.txtThe golden compare and a light perf smoke (no budgets — sandbox timing
must never flake the suite) run as ctests
(example_conformance_golden, example_conformance_perf); the CI
shader-demo artifacts bundle conformance(.exe) with tests-golden/ +
tests-data/ so the golden suite runs from the artifact too.
Fallback drill (conformance drill) is the library half of the
(Unavailable) fallback contract: every preset in
tests/golden/drill/manifest.txt must
FAIL to load, each exercising a distinct failure layer (comments-only
preset → parser; missing shader file; GLSL compile error; live-sampled
never-declared sampler; missing LUT image; absent #reference target;
PassOutput0 sampled in pass 0 — the exact class a fork session hit on
hardware, see PROVENANCE). Per case the drill requires a clean rejection
with a non-empty error, zero validation-layer messages, and a
pixel-identical re-render of a known-good preset (runner/blit.slangp)
on the same device — a loaded "broken" preset, a poisoned device or a
drifted recovery frame all exit 1.
./build/examples/conformance drill tests/golden tests/data # ctest: example_conformance_drill# from a CI shader-demo artifact (fixtures ride along in tests-golden/):
./conformance drill --manifest tests-golden/drill/manifest.txt tests-golden tests-datacmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failureOptions: SLANGVK_BUILD_SHARED (default ON, builds libslangvk.so/dll
alongside the static lib), SLANGVK_BUILD_TESTS (default ON),
SLANGVK_BUILD_EXAMPLES (default ON, builds preset_info,
shader_demo, corpus_sweep and conformance), SLANGVK_ENABLE_VK (default ON,
builds the Vulkan runner; OFF gives a core-only build) and the demo
options SLANGVK_DEMO_WINDOW / SLANGVK_DEMO_FETCH_SHADERS (see
Shader demo). SLANGVK_SANITIZE=address,undefined is
available for test builds. Size levers (opt-in, off by default):
SLANGVK_LTO enables IPO + -Oz release codegen for the glslang-bearing
library, and SLANGVK_SYSTEM_GLSLANG=ON links a system-installed glslang
via find_package(glslang CONFIG) instead of the pinned FetchContent copy.
glslang, Vulkan-Headers, volk and VMA are fetched and built automatically
(SHA-256 pinned; see PROVENANCE.md).
CI (.github/workflows/ci.yml) tests every
push and uploads the built libraries as workflow artifacts — the
per-toolchain distribution the plan prescribes for the C++23
same-toolchain-ABI API (include/slangvk/export.h):
libslangvk-windows-x64-mingw—libslangvk.dll(glslang + volk linked in),libslangvk.dll.aMinGW import library,libslangvk.astatic library,include/slangvk/headers. MSYS2 MinGW-w64 GCC build; link the DLL from MinGW-w64, not MSVC.libslangvk-linux-x64—libslangvk.a,libslangvk.so, headers; GCC Release + Debug builds gating the full ctest suite on lavapipe with the Khronos validation layer installed.shader-demo-linux-x64/shader-demo-windows-x64— the runnable demo (windowed GLFW build on Linux; headless-capable on Windows) bundled withcorpus_sweep/corpus_sweep.exe,conformance/conformance.exe, the demo presets,testcard.png, the golden-image kit (tests-golden/: manifest + images + budgets) and the fixture presets (tests-data/) — the golden suite runs straight from the artifact withconformance golden --manifest tests-golden/manifest.txt --golden-dir tests-golden tests-data .— plus a USAGE.txt covering the tools. Assets resolve relative to the executable, so the zip runs from anywhere.
A sanitized (address + undefined) Linux test build runs alongside the
Release jobs — with synchronization validation enabled whenever the
Khronos layer is present. A provenance job runs
tools/scan_codefindings.py (the plan §4
clean-room gate: RetroArch fingerprint scan) on every push. The Vulkan
runner tests execute against lavapipe on Linux; on Windows they skip
cleanly when no ICD is present and everything else still runs.
The full-corpus compile-sweep ledger runs separately:
.github/workflows/corpus.yml shallow-
clones libretro/slang-shaders at the revision pinned in the committed
baseline header (nightly, or via workflow_dispatch), sweeps every preset
headlessly on lavapipe, and diffs the fresh ledger against
corpus/baseline-ledger.txt — with the
no-regression gate ON since 2026-10-18: a preset that loads in the
baseline but fails the fresh sweep fails the run, while FAIL→LOAD fixes are
tolerated and reported as review material. Refreshing the pinned baseline
itself is a deliberate, reviewed procedure documented in the workflow
header.
- docs/SPEC.md — the preset-format contract (parsing, translation and execution semantics, section by section).
- docs/API.md — the public C++23 API reference (draft,
pre-freeze): core + runner layers, error model, lifetimes, walkthrough.
(The C ABI's contract lives in the
c.hheader comment.) - docs/PLAN.md — milestones, API sketch, risk register.
- docs/CHECKLIST.md — per-item M0–M4 implementation status, audited against the plan.
- docs/CHANGELOG.md — notable changes per milestone (M0 → current) plus the unreleased roadmap.
- audit.md — the codebase audit: the open findings, their severity and the recommended fixes.
- deltas.md — the closed findings: what each one was, what actually shipped, and how it was verified.
- corpus/baseline-ledger.txt — the pinned per-preset corpus baseline (revision, totals, LOAD/FAIL per preset).
- PROVENANCE.md — upstream provenance, documented divergences, pinned dependency hashes and the corpus ledger.
MIT — see LICENSE. The plan (§4) defines the provenance policy: clean-room against RetroArch's GPL slang implementation; no upstream DevilutionX code; vendored dependencies limited to permissively licensed ones.