Shader Build Pipeline (Slang → SPIR-V/WGSL)#
How Slang becomes SPIR-V and WGSL here. AGENTS.md links here rather than
restating it.
How it works#
Shaders are Slang under Resources/ShadersSlang/.
The build scripts (scripts/windows/Build-SlangShaders.ps1,
scripts/linux/compile-slang-shaders.sh) compile each .slang file to:
SPIR-V (
.spv) for the C++ Vulkan renderer →Resources/ShadersSlang/build/spirv/WGSL (
.wgsl) for the Rust WebGPU renderer →Resources/ShadersSlang/build/wgsl/. Each source in the manifest’swgslMapis also compiled without-entry/-stage, which puts all its entry points in one combined WGSL file, and copied into OxidANT’scrates/webgpu_renderer/src/shaders/orcrates/gui/src/shaders/, replacing the hand-written WGSL there.
The compile is wired into the builds, no opt-in flag:
scripts/linux/cmake-configure-build.sh compiles the Slang manifest before
every Linux build, and Build-Windows.ps1 before its clangcl-debug,
clangcl-release and msvc-release builds (not clangcl-profile or
msvc-debug), so a .slang edit reaches the Vulkan engine on the next such
build. A bare cmake --build compiles no shaders. slangc is resolved from
VULKAN_SDK\Bin, then PATH (the Vulkan SDK ships it; verified against
1.4.350.0).
Both scripts are thin wrappers holding only this project’s paths (manifest,
Slang source root, SPIR-V/WGSL output roots, and the repository root the
manifest’s wgslMap destinations resolve against). The driver (slangc
resolution, -I expansion, the manifest walk, staleness checking, the combined
WGSL emit with its patch table, the toolchain floor and the varying-location
validator below) lives in ANTfrastructure (linux/scripts/lib/slang-compile.sh
and windows/scripts/modules/WindowsSlang.Common.psm1), so every Slang project
gets the same guarantees. Change behaviour there, in both twins, and keep the
BuildIntegrity tests in step.
The C++ renderer loads pre-compiled SPIR-V with plain file I/O
(Kataglyphis::Shared::readBinaryFile, called from ShaderHelper.cpp); there
is no runtime shader compilation. Slang emits "main" as the SPIR-V entry
point name (not the Slang function name), so every pName in pipeline
creation is "main".
histogram.wgsl in the Rust crate is hand-written, with no Slang source:
Slang’s InterlockedAdd on RWStructuredBuffer is not supported for the WGSL
target (atomics require array<atomic<u32>> storage). That is the WGSL
fallback policy in shader-sharing.md.
Staleness rules#
An output is reused only when it is newer than its source and every
.slang file under the Slang tree and shader-manifest.json itself
(conservative: an import or manifest edit rebuilds every dependent). The
compile scripts walk the manifest and recompile only stale entries.
The BuildIntegrity gates that check already-compiled or checked-in output
(CompiledShadersAreNotOlderThanSharedIncludes,
CheckedInWgslIsNotOlderThanItsSlangSource, in
Test/commit/VulkanEngine/buildIntegritySuite.cpp) do not copy that
whole-tree rule: they compare each source against its import_closure, the
.slang files it (transitively) actually imports, and flag an artifact
stale only when one of those is newer. Rebuild-everything is right for
producing outputs; used to check a built tree, the “newest file anywhere
under common/” rule flagged artifacts whose real dependencies never changed,
which turned every checked-in WGSL red after an edit to one material module.
One gap remains either way: a dependent shader whose emitted output is
byte-identical after a common/ edit is never re-copied, so its mtime stays
behind and the per-shader gate still reports it stale. Fixing that needs a
content stamp from the compile scripts themselves (upstream in
ANTfrastructure), not a smarter mtime comparison here.
The manifest is data, in one place#
Resources/ShadersSlang/shader-manifest.json holds the shader list (source
file, entry point, stage, targets), the combined-WGSL copy map and the
depth-texture patch table. BOTH compile scripts read it (PS via
ConvertFrom-Json, bash via python3, which is required and fails loud when
missing; jq is not in the ANTfrastructure Linux image). Add or retarget a
shader by editing the JSON, never the scripts: before this the two scripts had
drifted four ways, including Linux silently skipping compilation when slangc
was absent. _comment fields in the JSON carry the rationale that used to be
code comments.
The combined WGSL emit needs slangc ≥ 2026.8#
WGSL requires @location(N) on every non-builtin member of an inter-stage
(varying) struct. slangc 2026.1-52-gc8ddf20bb (the build in Vulkan SDK
1.4.341.1, which the ANTfrastructure Linux image put on PATH when this was
found: then :latest-cross with VULKAN_VERSION=1.4.341.1) drops that
attribute in the combined emit (no -entry/-stage, which is how every
wgslMap file is produced), turning
@location(0) uv_0 : vec2<f32>,
into a bare uv_0 : vec2<f32>, that wgpu/naga rejects. The per-entry emits
from the same binary are correct, so it is a whole-module-emit bug, not a
layout-assignment one, and not ours: the scripts copy slangc’s output verbatim
apart from the depthTexturePatches regexes (and bloom.wgsl, one of the
affected files, has no patches at all).
It is not a Windows-vs-Linux divergence, though it looked like one while
the Windows host ran a newer SDK than the container. Verified 2026-08-02:
slangc 2026.8 emits @location correctly, and its Linux output is
byte-for-byte identical to the checked-in (Windows-generated) files; the
only Windows/Linux difference is CRLF vs LF, which .gitattributes normalises
away.
Two guards, in shader-manifest.json’s minSlangcVersionForWgsl (2026.8)
and in both compile scripts:
Below the floor the combined emit is skipped entirely, with a loud warning. SPIR-V still compiles, and the checked-in WGSL is left untouched instead of being overwritten with output naga rejects. After editing a
.slangthat feedswgslMap, regenerate on a toolchain at or above the floor;BuildIntegrity.CheckedInWgslIsNotOlderThanItsSlangSourcefails if that is forgotten.At or above the floor the emit is verified before it is copied: a struct with at least one
@builtin/@locationmember (an IO struct) and a member with neither fails the script, which names the file and lines and does not copy it into the Rust crate. An emit regression cannot be committed silently either.
BuildIntegrity.CheckedInWgslVaryingStructsCarryLocations applies the same
rule to the checked-in files on every CI platform, and
BuildIntegrity.ShaderManifestPinsAMinimumSlangcVersionForWgsl keeps the floor
from being deleted. Raise the floor rather than hand-editing generated WGSL;
bumping VULKAN_VERSION in the ANTfrastructure image re-enables WGSL
regeneration on Linux. The pinned hub’s versions.env names
VULKAN_VERSION=1.4.357.0, whose Windows SDK ships slangc 2026.13.1 (read
from the host SDK on 2026-09-25), above the floor. Whether the published
:latest carries this SDK was not checked here.
Fast shader iteration#
Edit a .slang file, then recompile:
pwsh -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows\Build-SlangShaders.ps1
bash scripts/linux/compile-slang-shaders.sh
The C++ engine loads the .spv files at startup through
Kataglyphis::Shared::readBinaryFile (File::readCharSequence until
2026-08-04), so a shader-only change needs no C++ rebuild: recompile the
shaders and run.
Historical note: the retired GLSL/glslc pipeline (until the Slang migration)#
Shaders used to be hand-written GLSL under Resources/Shaders/, compiled per
file by compile-shaders.ps1 via glslc, with staleness checked separately by
ShaderHelper::compileShader at runtime. That tree and script are gone:
Resources/ holds only ShadersSlang/, Models/ and Textures/, and the two
scripts under “How it works” are the only compile path. Two lessons from then
shaped the Slang scripts:
Silent reuse of a stale output is the worst failure mode. The GLSL pipeline reused a
.spvwhenever the file existed, without comparing timestamps, so a shader edit could go unexecuted while the GPU kept running the old binary. It cost real debugging time on a shadow-rendering issue that turned out to be stale SPIR-V, not a logic bug. The Slang staleness rule above (reuse only when newer than source and every.slangfile) exists because of it.A compiler warning nobody acts on is indistinguishable from success. A
glslcinclude-path bug silently failed to compile the main rasterizer fragment shader on every build (only aWrite-Warning, the previous.spvleft on disk, build reported success) and was misdiagnosed as an intentional legacy limitation.BuildIntegrity.EveryShaderSourceHasCompiledBinarymakes a silently skipped shader source fail a test instead of aging invisibly.
Known gaps#
Resources/ShadersSlang/build/(compiled.spv/.wgsloutput) is gitignored, so a fresh clone must run a compile script before the engine has anything to load. The staleness checks only protect an existing checkout; they do not replace the first compile.