ShaderCompiler
Offline shader preprocessing tool of Jazz² Resurrection.
ShaderCompiler** is a standalone command-line tool in "Sources/Utilities/ShaderCompiler". It reads annotated .shader files from "Sources/Shaders", expands their variants, performs GLSL declaration reflection offline, and emits self-contained C++ headers with the sources and reflection data, so the runtime no longer needs glGetActiveUniform introspection or double compilation of batched shaders to size the std140 InstancesBlock. Beside that primary job it hosts every source-to-source lowering the non-GL backends need: an HLSL transform for Direct3D 11, a Vulkan GLSL transform with offline SPIR-V compilation, an ESSL 100 transform for the OpenGL|ES 2.0 profile, a GLSL-to-C++ transpiler for the software renderer, and a fixed-function transpiler for the console backends that have no fragment shaders at all (PVR on Dreamcast, GX on Wii/GameCube, GU on PlayStation Portable).
A .shader file therefore describes an effect on two tiers:
- Shader backends get real GLSL — the
void vertex()/void fragment()entry points, lowered per variant and then transformed per backend. The software renderer is still a shader backend in this sense: its fragment function is the same GLSL transpiled to C++ ahead of time (ShaderCompiler::GlslToCpp), not a hand-written CPU path. - Fixed-function backends get a
void fixed_function()block — a short list of hardware passes over the sprite quad, written next to the GLSL it approximates and transpiled into C++ that drives the console's fixed-function pipeline. The design rationale lives in"Docs/FixedFunctionShaderDesign.md"; the runtime contract in"Sources/nCine/Graphics/RHI/FixedFunctionPass.h".
The tool is plain C++17 with zero dependencies beyond the standard library and is portable across MSVC**, GCC and Clang. All of its outputs are committed to the repository under "Sources/Shaders/Generated" — the game's build never runs the tool and never embeds a .shader file, so the headers have to be regenerated manually after editing a shader, see the workflow section.
Which artifact serves which platform
Every backend consumes one artifact, and every artifact is produced by one mode of the tool. The per-shader header carries the modern GLSL plus its three per-backend lowerings side by side, so a single header serves GL, ES2, D3D11 and Vulkan; the other tiers get their own aggregate headers.
| Backend | Artifact it consumes | Produced by |
|---|---|---|
| OpenGL 3.3, OpenGL|ES 3.0, WebGL 2.0 | VsSource / FsSource of the per-shader header (the #version header and platform defines are injected by the engine at runtime) | primary mode |
OpenGL|ES 2.0 profile (NCINE_RHI_GL_PROFILE=ES2) | VsSource100 / FsSource100 — the ESSL 100 lowering baked into the same header | primary mode |
| Direct3D 11 | HlslVsSource / HlslFsSource — Shader Model 4/5 HLSL baked into the same header | primary mode |
| Vulkan | VkVsSpirv / VkFsSpirv — SPIR-V words compiled offline through glslangValidator | primary mode + glslang |
| Software renderer | "SwGeneratedShaders.h" — transpiled C++ fragment functions | --emit-sw-generated |
| Dreamcast (PVR) | "PvrGeneratedEffects.h" — transpiled fixed_function effects | --emit-fixed-function pvr |
| Wii / GameCube (GX) | "GxGeneratedEffects.h" — transpiled fixed_function effects | --emit-fixed-function gx |
| PlayStation Portable (GU) | "GuGeneratedEffects.h" — transpiled fixed_function effects | --emit-fixed-function psp |
| PlayStation Vita (GXM) | "CgGeneratedShaders.h" — Cg stage sources compiled on the console | --emit-cg |
Fields whose lowering was unavailable are emitted as null: a construct outside the HLSL or ESSL 100 subset leaves those sources null, and a generation run without glslang leaves the SPIR-V null. A program whose fixed_function block is missing is simply absent from the console tables and is skipped at runtime with a one-time warning; a shader the software transpiler declines is likewise absent from "SwGeneratedShaders.h".
Runtime-compiled .shader files (the engine links the parser, reflection and CompileRuntimeProgram — see ShaderCompiler::
Building the tool
The desktop CMake build adds the tool automatically as a utility target in the Utilities folder. It is skipped when cross-compiling (Emscripten, Nintendo Switch, Vita, UWP, Wii, GameCube and Dreamcast) because it always runs on the build machine. It can also be built completely standalone:
cmake -S Sources/Utilities/ShaderCompiler -B build-shadercompiler cmake --build build-shadercompiler
On Windows, GenerateAll.ps1 expects the executable at "Sources/Utilities/ShaderCompiler/x64/Release/ShaderCompiler.exe", which is where the included ShaderCompiler.vcxproj puts it. The tool target also compiles RuntimeShader.cpp — the runtime facade that is built into the game as well — so drift between the offline and the runtime path is caught early.
Command-line reference
ShaderCompiler <input.shader> -o <output.h> [-n <namespace>] [--glslang <path>] ShaderCompiler <input.shader> --check | --essl100-check | --hlsl | --cg | --vulkan ShaderCompiler --emit-types <output.h> ShaderCompiler --emit-sw-generated <output.h> <input.shader ...> ShaderCompiler --emit-cg <output.h> <input.shader ...> ShaderCompiler --emit-fixed-function <pvr|gx|psp> <output.h> <input.shader ...> ShaderCompiler --hlsl-check <input.shader ...> ShaderCompiler --spirv-check [--glslang <path>] <input.shader ...>
The six --emit-* / *-check standalone modes are recognized only as the first argument; everywhere else they are rejected as an unknown option. Everything else is the primary per-shader mode, which takes exactly one input file.
| Mode or option | Meaning |
|---|---|
-o <output.h> | Path of the generated C++ header. Required unless one of the four dump switches is given |
-n <namespace> | Namespace for the generated program data (default ShaderArtifacts); :: nesting is allowed, an empty name, a leading digit or any character outside [A-Za-z0-9_:] is an error |
--glslang <path> | glslangValidator used to compile the embedded SPIR-V; otherwise discovered via VULKAN_SDK\Bin, VULKAN_SDK\Bin32 and PATH |
--check | Human-readable reflection dump of every program and variant — structs with std140 offsets, uniforms, blocks with BaseSize/InstanceStride, texture units, attribute locations. The test suite compares these dumps against "tests/expected" |
--essl100-check, --target essl100 | ESSL 100 (OpenGL|ES 2.0) transform of every variant's stage sources. essl100 is the only accepted --target value |
--hlsl | HLSL (Shader Model 4/5) transform of every stage |
--cg | Cg transform of every stage, in the dialect the PS Vita's sceGxm backend compiles (the same emitter as --hlsl, see below) |
--vulkan | Vulkan GLSL ("#version 450") transform of every stage (does not require glslang) |
--emit-types <output.h> | Writes the shared reflection-types header and nothing else |
--emit-sw-generated <output.h> <in ...> | Transpiles the fragment stage of every variant of every input to C++ and writes the aggregate software-renderer header |
--emit-cg <output.h> <in ...> | Transforms every variant of every input to Cg and writes the aggregate PS Vita header |
--emit-fixed-function <pvr\|gx\|psp> <output.h> <in ...> | Transpiles the applicable fixed_function block of every variant of every input and writes the aggregate per-backend effect header |
--hlsl-check <in ...> | Emits the VS + PS HLSL of every variant and compiles each stage via D3DCompile (vs_5_0 / ps_5_0), printing a pass/fail table (Windows only, uses d3dcompiler_47.dll) |
--spirv-check [--glslang <path>] <in ...> | Emits the Vulkan GLSL of every variant and compiles each stage to SPIR-V via glslangValidator, printing a pass/fail table |
--help, -h, /? | Prints the usage text (to stderr) and exits successfully |
The five dump switches write nothing — they print to stdout and never touch the committed artifacts. They are not mutually exclusive, but they are ordered: when several are combined, the first of --essl100-check, --hlsl, --cg, --vulkan, --check wins and the others are silently ignored. --glslang is ignored by every dump path.
Diagnostics go to stderr in three shapes: "<file>:<line>: error: <message>" for anything the parser, reflection or an emitter reports, "<file>: error: <message>" for a failed #include expansion or an unreadable input, and a bare "error: <message>" for command-line and I/O problems. The exit code is 0 on success, 1 for an input or emission failure, and 2 for a usage error.
The aggregate modes in practice
--emit-types writes the reflection types header, which every generated header includes; the types live in the fixed ShaderCompiler namespace, deliberately independent of -n, so headers generated with different data namespaces can be included together.
--emit-sw-generated never fails on unsupported input: shaders outside the transpiler's subset are declined** and simply omitted, and the printed summary lists each one with its reason. It is also the only path that builds stage sources with SOFTWARE_RENDERER defined, see the conditionals below.
--emit-cg is an aggregate for a different reason than the other two: its output could have been two more fields on the per-shader header, but rewriting those headers requires Windows (that is where the DXBC and SPIR-V blobs in them come from), so a Cg regeneration on any other host would silently drop them. A separate header keeps the two independent — a Cg run touches nothing else. Like --emit-sw-generated it never fails on unsupported input: a variant the transform declines is omitted and listed in the printed summary, and the backend then reports that shader as unavailable at load time.
--emit-fixed-function is the opposite: an invalid block is a hard error that fails the whole run, because a block is authored intent, see below.
The .shader input format
A .shader file is a custom shader language: GLSL globals plus void vertex() / void fragment() entry points, annotated with plain keyword directives — top-level statements at brace depth 0, terminated by ;. Comments follow GLSL rules and pass through into the emitted sources. Do not put #version in the input — the engine injects the version header ("#version 330", "#version 300 es", "#version 100") and the platform defines at runtime. "Sources/Utilities/ShaderCompiler/README.md" is the full in-tree specification, including the std140 layout rules and the --check dump format.
Top-level directives
| Directive | Meaning |
|---|---|
program <Name>; | Required, exactly once, and it has to precede shader_type, variant, batched, precision and the entry points — in practice, write it first. A C++/GLSL identifier; it names the emitted Program and is half of the identity the console tables are keyed by |
shader_type canvas_item; shader_type custom; | Optional — the default is custom. canvas_item opts into the sprite-template lowering |
variant <NAME>; | Declares an optional variant; may appear multiple times. The output contains the unnamed base variant (always Variants[0], its name is empty) plus one entry per declaration, compiled with #define <NAME> (1) baked in. There are no cross-products — each variant is exactly one define |
render_mode <mode>[, <mode>]; | Zero or more of blend_mix, blend_add, blend_sub, blend_mul, blend_premul_alpha, unshaded, OR-ed into the RenderModes bitmask on the emitted program. May repeat |
precision mediump; precision highp; | Optional (default mediump), those two spellings only — the float precision of the auto-emitted "#ifdef GL_ES" fragment prologue. Only the two-token form is a directive; a real GLSL statement with a type (precision highp float;) passes through as ordinary GLSL |
batched <Name>; | canvas_item only — also emits the batched twin program (InstancesBlock + the 6-vertex corner formula) into the same header, sharing the fragment stage, the variants and the fixed_function blocks. An error in custom mode |
#include "relative/path" | Replaced textually before parsing, recursively up to depth 8, relative to the including file, so the generated artifacts stay self-contained. Line numbers in diagnostics refer to the include-expanded stream |
void fixed_function([<target>[, <target>...]]) { ... } | Console fixed-function implementation of the effect, see below. Empty parentheses declare the generic block, one target overrides it for that backend, a comma-separated list (void fixed_function(pvr, psp)) declares one implementation shared by several. Never part of the GLSL stages — a file that carries a block emits a byte-identical per-shader header |
Declarations shared by both modes
| Element | Meaning |
|---|---|
varying [flat] [precision] <type> <name>; | Lowered to an out in the vertex stage and an in in the fragment stage, qualifiers preserved. May be wrapped in a global-scope SOFTWARE_RENDERER conditional, see below |
attribute [layout(location = N)] <declaration>; | A vertex attribute — emitted as an in global in the vertex stage only. A leading layout(...) stays in front of the in keyword and the location is honored by reflection |
uniform <type> <name> : <hint>[, <hint>]; | texture_unit(N) assigns the texture unit (0–31) of a sampler uniform and is the only way to assign one explicitly; in the primary mode the name must match a sampler in at least one variant, and two hints naming the same uniform are an error. source_color, hint_range(...), filter_nearest, filter_linear, repeat_enable and repeat_disable are parsed and dropped, anything else is an error. The hint list is stripped from the lowered declaration, so hints never reach the emitted sources |
| everything else | Global scope — uniforms, std140 blocks, consts, structs, helper functions — is shared by both stages of every lowered document |
Custom mode (the default)
No template and no built-in substitutions: user identifiers pass through untouched. Both entry points are required. The body of void vertex() becomes the vertex main() verbatim and writes gl_Position itself. The body of void fragment() is lowered with "out vec4 COLOR;", where COLOR is the fragment output variable itself — undefined until written, with no default and no epilogue, so an early return; is safe. Referencing fragColor anywhere in a .shader file is a parse error — that name does not exist, write COLOR.
Canvas mode (shader_type canvas_item)
Opts into the engine's standard sprite template (InstanceBlock / InstancesBlock and the vTexCoords / vColor / vPaletteOffset varyings) and makes the batched twin available.
void vertex() is optional: the generated main() computes the built-ins as locals, splices the body verbatim, then runs the standard epilogue (gl_Position plus the varying stores). Because of that epilogue, return; inside a canvas vertex() is a parse error. void fragment() is required and enters with COLOR holding the instance color.
| Built-in | Vertex stage | Fragment stage |
|---|---|---|
VERTEX | sprite-local position in pixels, writable | — |
UV | texture coordinates, writable | substituted with vTexCoords |
COLOR | instance color, writable | enters as the instance color, and is the output |
PALETTE_OFFSET | palette row of the instance, writable | substituted with vPaletteOffset |
TEXTURE | — | substituted with uTexture |
A canvas document that references TEXTURE without declaring uTexture gets "uniform sampler2D uTexture;" auto-declared with texture unit 0; an explicit declaration, with or without a texture_unit(N) hint, wins. The four fragment built-ins are the whole set: the names NORMAL, SCREEN_UV, SCREEN_PIXEL_SIZE, TIME, POINT_COORD — and VERTEX, which exists only in the vertex stage — are reported as unsupported rather than silently passed through, in void fragment() and in the shared globals alike.
The template's built-ins are ordinary locals of the generated main(), so redeclaring one in the body is a GLSL redeclaration error. When a canvas document has no void vertex() at all, the vertex stage is the template alone — the shared globals are not emitted into it, since nothing there could reference them.
Variants, conditionals and includes
Reflection must run per variant, so the tool contains a mini preprocessor implementing the object-like subset of the C preprocessor (#define / #undef, #if / #ifdef / #ifndef / #elif / #else / #endif with integer constant expressions). It produces the declaration stream fed to the reflection parser — the emitted sources are not preprocessed output, they keep the original text verbatim with only the variant define and "#line 1" baked at the top. Two rules are special: GL_ES is never predefined (reflection is taken from the desktop GL view) and BATCH_SIZE is symbolic — it evaluates as 1 inside #if expressions but stays symbolic when used as an array size.
Three conditionals are resolved at compile time and therefore never survive into any emitted source:
"#ifdef VERTEX_STAGE"/"#ifdef FRAGMENT_STAGE"(with an optional"#else") select which stage sees a shared global. They are rarely needed anymore — attributes and varyings have their own keywords, stage-specific helper functions need no guards because unused functions are eliminated per stage, and everything else is harmless when shared."#ifdef SOFTWARE_RENDERER"/"#ifndef SOFTWARE_RENDERER"is resolved when the stage source is built, and only the--emit-sw-generatedpath builds with it defined. A shader can therefore carry a cheaper CPU variant of an expensive fragment path without changing any other backend's output — see"TexturedBackground.shader", whose software branch approximatespow(distance, 1.5)and drops the per-pixel star field. Wrapped around global-scopevaryingdeclarations** it goes one step further and gives the software renderer a different set of varyings: the tool consumes the directive lines while parsing, tags each declaration and re-wraps it per stage, so a conditional there may contain nothing butvaryingdeclarations (and no nested directives)."tests/SwVarying.shader"is the worked example —vPosfor the shader backends,vRectfor the software one.#ifdef <VARIANT>on a variant name resolves per variant, in the GLSL stages and inside afixed_functionblock alike.
Only those exact #ifdef / #ifndef spellings are recognized: #if defined(...) is not a form any of the three resolvers accepts — for the stage macros it is a hard error, and for SOFTWARE_RENDERER the macro name simply leaks into the emitted source. Every other conditional passes through textually, and nesting works in both directions.
Automatic cleanup passes
The emitted stage sources keep the original text verbatim, but several conservative passes run per stage, in this order:
- Stage conditionals — resolved as described above; no stage macros survive.
- Unused-function elimination — every global function never referenced outside its own definition is removed, iterating to a fixpoint with
main()as the root. - Unused-varying trimming — a varying the fragment stage never reads is removed together with its provably dead vertex-stage stores, or demoted to a plain global when a store has side effects.
- Unused-uniform/block elimination — per-stage dead uniforms, std140 blocks, defines and structs are removed under a hard reflection-preservation rule: a declaration leaves a stage only when the same declaration survives in the other stage, so the merged per-variant reflection is byte-identical before and after the pass.
- Constant folding — literal-only subexpressions inside function bodies are folded with exact GLSL semantics. Global-scope declarations are never touched, which again guarantees reflection stays byte-identical.
Reflection and generated headers
Reflection runs per stage and per variant over the global-scope declarations (structs, loose uniforms, "layout (std140)" uniform blocks with computed offsets and strides, sampler2D / sampler3D / samplerCube texture bindings, vertex attributes), and the two stages are merged into one program-level view, GL style. BATCH_SIZE is symbolic: an array sized by it records its element stride as InstanceStride on the block and marks the count with a sentinel, and the runtime computes the batch size as maxUniformBlockSize / InstanceStride — the engine's batched sprite instance struct yields a 112-byte stride, matching the default 585 = 65536 / 112.
Each generated header is self-contained (only <cstdint> / <cstddef> plus the shared types header) and carries, per variant:
- The stage sources as raw string literals —
<Program>_Vs/_Fsfor the base variant,<Program>_<VARIANT>_Vs/_Fsfor named ones — each starting with the variant define and"#line 1" - The ESSL 100 and HLSL lowerings of those sources (
_Vs100/_Fs100,_HlslVs/_HlslFs), and the offline-compiled SPIR-V words when glslang was available - Reflection as
constexprarrays of plain structs (Uniform,BlockMember,UniformBlock,TextureBinding,Attribute), tied together byProgramVariantandProgram
The reflection types live in "Generated/ShaderCompilerTypes.h" in the fixed ShaderCompiler namespace; program data goes into the -n namespace. Engine code can include the types header alone to consume reflection without pulling in any program's data.
Console fixed-function blocks
The console backends (PVR on the Dreamcast, GX on the Wii/GameCube, GU on the PlayStation Portable) have no fragment shaders — an effect there is a short list of passes over the sprite quad, each a small bundle of hardware state. A fixed_function block states that pass list in the shader file itself, next to the GLSL it approximates. This is the whole console tier of "WhiteMask.shader":
void fixed_function() { pass p; p.color = vec4(0.0, 0.0, 0.0, COLOR.a); p.offset_color = COLOR.rgb; submit_quad(p); }
See "Docs/FixedFunctionShaderDesign.md" for why the language looks like this and what each console can actually do, and ShaderCompiler::
Block selection and what gets emitted
A block's parentheses hold its target list:
| Spelling | Serves |
|---|---|
void fixed_function() | every backend without a more specific block — the generic implementation, restricted to the portable core |
void fixed_function(pvr) | the Dreamcast only |
void fixed_function(gx) | the Wii/GameCube only |
void fixed_function(psp) | the PlayStation Portable only |
void fixed_function(pvr, psp) | both of those, from ONE body — the list form |
void fixed_function(psp, gx, pvr) | any subset in any order. Whitespace is free: (pvr,psp) and ( pvr , psp ) are the same declaration |
A block that names a backend — on its own or inside a list — wins over the generic block for it regardless of declaration order**, and every target belongs to exactly one block per file: a target claimed twice is an error whether it was spelled singly or inside a list. The list form is for the case where two consoles reach an effect with literally the same code; when the bodies genuinely differ, they stay separate blocks. Capabilities are then checked against the intersection of the listed targets.
Today Colorized.shader, FrozenMask.shader, PartialWhiteMask.shader and the two background shaders carry a shared void fixed_function(pvr, psp) block (the two no-combiner tiers) plus a void fixed_function(gx) one, and Transition.shader carries a void fixed_function(pvr) block plus a shared void fixed_function(gx, psp) one (the two 16-vertex strip scratches); everything else has a single generic block — which is the norm, since a program with no block at all for a backend is skipped at runtime there.
The --emit-fixed-function mode transpiles the applicable block of every program variant — the block is preprocessed once per variant with the variant define baked in, exactly like the fragment stage — into the body of a "void <Program>[_<VARIANT>]_Effect(EffectContext&)" C++ function, and collects them into one aggregate header per backend with a FixedFunctionGeneratedEffects[] table of
{ program, variant, usesOffsetColor, requirements, intrinsic, &function }
entries, keyed by the true (program, variant) identity the loaders plumb in with ShaderProgram::SetProgramIdentity() — no shader name is ever matched in a backend. An entry carries either a function or an intrinsic, never both. Programs without an applicable block are absent from the table and skipped at runtime with a one-time warning. The including device file supplies the concrete EffectContext through a using alias before the include; the generated header is otherwise self-contained, carrying its own small ff vector runtime so that FixedFunctionPass.h stays the only contract between the generator and the backends.
Two fields are computed statically per (program, variant) while the body is emitted:
usesOffsetColor— whether any reachablep.offset_color = ...assignment exists, because the PVR needs it when compiling the base polygon header (specular enable is per program, not per pass). It is derived from the selected body, so it can differ per backend: PartialWhiteMask istrueon the PVR and the PSP, which lift the sprite with an offset colour, andfalseon the GX, which uses the combiner's x2 output scale instead.requirements— aFixedFunctionRequirementsbitmask of the optionalEffectContextfacilities the function can ever call:NeedsTexelStep(texel_size()/has_texel_size()),NeedsUniforms(has_uniform()/uniform_vec2/vec4()),NeedsStripBuilder(strip_*()/submit_strip[_shaded]()) andNeedsQuadAxes(quad_origin()/quad_axis_x/y()). The backends'Dispatchgates the matching per-draw context setup on these bits and skips the rest; because the bits come from the same analysis that emitted the calls, this can never change what a function submits.Byte-identical bodies are deduplicated.** Emitted bodies deliberately carry no program name, so batched twins and palette variants — which differ only in the dispatch loop's instance decoding, not in their pass code — come out identical and collapse into one function, named after its first occurrence and preceded by a provenance comment: a
from <file>:<block>line naming where the body came from, plus a// Shared by:line listing every (program, variant) that points at it, each bracketed with its own origin when it came from a different file. Today's 39 table rows contain 6 intrinsic bindings and 33 effect rows, which collapse to 10 emitted functions on each of the three backends.
Pipeline bindings
A block whose sole statement is pipeline <name>; does not describe passes at all: it binds the program to a backend pipeline stage that consumes an engine data structure, and the table entry carries the FixedFunctionIntrinsic value instead of a function pointer. Anything else in such a block is an error — a block either describes passes or names a stage.
pipeline name | FixedFunctionIntrinsic | Stage | Declared by |
|---|---|---|---|
tile_map_mesh | TileMapMesh | a whole tile layer as one triangle-list mesh (the 8-float TileMap::AppendTileQuad contract, with quad-pattern recognition and, on the PVR, scissor clipping) | TileMapMesh.shader, TileMapMeshPalette.shader |
lighting_combine | LightingCombine | the viewport compositor — the direct-tier CPU-lightmap lighting hook | Combine.shader, CombineWithWater.shader, CombineWithWaterLow.shader |
line_strip_mesh | LineStripMesh | vertex-fed textured line strip of the weapon wheel (the PVR expands segments into thin quads; the GX and the GU draw native lines) | DefaultMeshSprite.shader |
The geometry-synthesized quad effects are not intrinsics — the transition iris and the warped background are ordinary transpiled blocks built on the strip builder.
Passes and their fields
pass p; declares a FixedFunctionPass local starting from the engine defaults (a plain modulated sprite pass with the material's own blending). It takes no initializer, each pass needs its own statement, the fields are write-only — reading one back is an error — and only plain = assignment is accepted.
| Field | Type | Meaning |
|---|---|---|
p.color | vec4 | per-vertex colour of the pass (the PVR argb, the GX raster colour, the GE vertex colour) |
p.offset_color | vec3 | post-texture additive term (the PVR offset colour; the GX runs a silhouette pass instead; the GU expands the pass into modulate + additive silhouette, which is the same result). Writing it is what enables it, and what sets usesOffsetColor |
p.screen_offset | vec2 | displacement of the whole quad in the quad's own coordinate space (the Outline ring taps) |
p.luma_gain | float | how much LUMA_RAMP amplifies the texel's luminance before saturating it; ignored by every other preset |
p.blend | enum | blend override for this pass |
p.tev | enum | GX combiner preset — portable intent, the PVR always modulates and the GE only knows MODULATE and SILHOUETTE |
p.blend | BlendMode | Hardware |
|---|---|---|
MATERIAL | Material | whatever the material configured (the default) |
ADD | Additive | additive glow and split-multiplier passes. The GX maps it to ONE + ONE; the PVR deliberately maps it to SRCALPHA + ONE — the additive mechanism its split-multiplier passes have always used, whose contributions are scaled by the pass alpha, so the mapping stays bit-identical with the code it replaced |
OPAQUE | Opaque | ONE + ZERO |
ALPHA | Alpha | plain source-alpha over (SRCALPHA + INVSRCALPHA), independent of the material — the warp's horizon tint runs over a material whose own blend does not apply to that pass |
p.tev | TevPreset | Meaning |
|---|---|---|
MODULATE | Modulate | texture * vertex colour (the default) |
SILHOUETTE | Silhouette | vertex colour where the texture has alpha (flat masks, shadows, glows) |
MODULATE_X2 | ModulateX2 | modulate with the combiner's x2 output scale. Not on the PSP — the GE has no output scale, so every block whose targets include psp rejects it |
MODULATE_X4 | ModulateX4 | modulate with the combiner's x4 output scale. Not on the PSP, same reason |
TINT_MIX | TintMix | mix(texel, colour, alpha) with an opaque result, one combiner stage. GX only — needs a block targeting gx and nothing else |
LUMA_RAMP | LumaRamp | silhouette whose tone is picked per texel from a two-endpoint ramp — color.rgb is the tone at luminance 0, offset_color the tone at 1, with luma_gain amplifying and saturating the texel's Rec.601 luminance in between; coverage stays texel alpha * color.a. GX only — needs a block targeting gx and nothing else |
The two GX-only presets need the programmable TEV combiner — the CLX2 can modulate a texel by the vertex colour and add an offset colour, and that is its whole vocabulary, while the GE has five fixed texture functions over one texel and the fragment colour — so using them outside a block targeting the GX alone is a hard error rather than a silently wrong console frame. A generic block is rejected too, because it is transpiled for every backend, and so is a target list such as void fixed_function(gx, psp), because the GE it also serves cannot express them.
The two output scales are the mirror image: the GE has no scale stage at all, so they are rejected for every block the psp target can reach — a psp block, a target list naming psp, and a generic one alike. Unlike the GX-only presets this is not a per-block capability — the PVR silently ignores p.tev, so a pvr block may keep using one — but a shared block using an output scale would be honoured by only some of the consoles it serves, which is precisely the "silently depends on
one console's feature" case these checks exist to prevent. On the no-combiner tiers a boost is expressed as passes instead: Colorized.shader splits its multiplier into up to three additive passes in its shared pvr, psp block.
p.offset_color is a third capability gap, and the one place where the answer is mechanism rather than policy: the GE has no post-texture additive term either (GU_TFX_ADD adds the texel to the fragment colour, not a third value), so the GU's EffectContext::SubmitQuad expands a pass carrying an offset colour into the modulated sprite plus an additive silhouette pass over it. With a = texel.a * color.a the pair produces dst*(1 - a) + a*texel*color.rgb + a*offset — term for term what the PVR's single specular-enabled draw produces — and collapses to ONE draw when color.rgb is zero, which is the mask/outline/shield idiom where the offset colour is the effect. Because the expansion lives in the backend, every generic** block that writes an offset colour keeps working unchanged on all three consoles; the GX does the analogous thing by reinterpreting the pass as its silhouette form.
Built-ins
The portable core is valid in every block. The extended vocabulary is reserved for blocks that NAME their backends (a single target or a target list — either way every backend it serves is spelled out), so a shared description can never silently depend on one console's geometry synthesis.
| Built-in | Type | Meaning | Generic block? |
|---|---|---|---|
COLOR | vec4 | the instance colour of the draw being dispatched, exactly the shader's COLOR input. Read-only, and cannot be redeclared | yes |
texel_size() | vec2 | displacement of one texel in the quad's own coordinate space, already converted per backend (raster space on the PVR, logical pixels on the GX, screen pixels on the GU) | yes |
has_texel_size() | bool | whether that step is derivable at all (a zero texRect has no scale) — blocks guard their texel_size() uses with it | yes |
submit_quad(p) | statement | submits one pass over the current instance's quad | yes |
quad_origin() | vec2 | pre-clip raster position of the sprite's (0,0) corner | no — pvr/gx/psp, alone or in a list |
quad_axis_x(), quad_axis_y() | vec2 | pre-clip raster displacements of the sprite's local axes. Synthesized geometry uses these instead of the post-scissor-clip corner arrays, so clipping cannot distort it | no — pvr/gx/psp, alone or in a list |
has_uniform(uName) | bool | whether the program resolved that uniform. An unresolved name loads zeros, so blocks guard with this exactly like the code they replaced null-checked its pointers | no — pvr/gx/psp, alone or in a list |
uniform_vec2(uName), uniform_vec4(uName) | vec2, vec4 | the program's resolved uniforms by name, through the backend's existing ResolveUniform machinery. The argument is an identifier, not a string literal | no — pvr/gx/psp, alone or in a list |
strip_position(i, <vec2>) | statement | strip-builder vertex position | no — pvr/gx/psp, alone or in a list |
strip_uv(i, <vec2>) | statement | strip-builder vertex UV, in the shader's texture space (the backend folds its padded-store scale) | no — pvr/gx/psp, alone or in a list |
strip_color(i, <vec4>) | statement | strip-builder per-vertex colour | no — pvr/gx/psp, alone or in a list |
submit_strip(p, count) | statement | submits the first count strip vertices textured, under the pass's flat colour | no — pvr/gx/psp, alone or in a list |
submit_strip_shaded(p, count) | statement | submits them with their per-vertex colours — gradients without a fragment shader. Untextured unless the pass's preset consumes the texel too (TINT_MIX), in which case the strip keeps its texture and UVs | no — pvr/gx/psp, alone or in a list |
The submission and strip calls are statements, not values; using one inside an expression is an error. The strip scratch is a backend capability — 8 vertices on the PVR and 16 on the GX and the GU, which both prefer fewer, longer primitives (on the GE every strip costs a draw call of its own) — and a literal vertex index outside 0 to capacity-1, or a literal count outside 3 to capacity, is a hard error, because at runtime an out-of-range index is dropped and an oversized count clamped, which would silently draw the wrong geometry. A block naming several targets is held to the minimum of their capacities, so void fixed_function(pvr, gx) gets the PVR's 8. Computed indices and counts stay unchecked.
Capability rules for a target list
A block that names several backends is validated against the intersection of what they can do — never against the backend whose header happens to be generated at that moment, which would accept a body that is silently wrong on the other backends the same block serves.
| Rule | On a target list |
|---|---|
| Extended vocabulary (strip builder, pre-clip quad axes, resolved uniforms) | allowed — every backend a list names is spelled out in it, so nothing is implicit. Only the generic block is held to the portable core |
TINT_MIX / LUMA_RAMP | allowed only in a block targeting gx and nothing else. void fixed_function(gx, psp) is rejected — the GE has no combiner |
MODULATE_X2 / MODULATE_X4 | rejected as soon as psp appears in the list. void fixed_function(pvr, psp) cannot use them even while the PVR's own header is being written |
| Strip-builder capacity | the minimum across the listed targets. void fixed_function(pvr, gx) is limited to 8 vertices, so a literal index 8 is an error there |
Each diagnostic names which of the block's own targets rejects the feature, so the fix — splitting the list back into separate blocks — is obvious from the message alone.
Statements and expressions
The block body is a plain statement list over a deliberately small type system: float, int, bool, vec2, vec3, vec4 and the opaque pass.
- Statements — local declarations (with initializers and comma-separated declarators),
passdeclarations, plain and compound assignment (=,+=,-=,*=,/=), prefix and postfix++/--on anintvariable,if/else, C-styleforwhose init declares or assigns anintcounter and whose condition is abool, braced blocks with their own scope, and the submission/strip calls above. Only those can stand alone as a statement. - Operators —
+ - * /on numeric operands, with a scalar broadcasting over a vector and mismatched vector widths rejected;%onintonly;< > <= >=on numeric scalars;==and!=on numeric scalars orbools;&& || ^^onbools; unary-on any numeric scalar or vector, unary!on abool; and the ternary?:, whose condition must bebooland whose branches must have the same type. - Functions —
min/maxtaking(float, float),(int, int),(vecN, vecN)or(vecN, float);clampandmixin the corresponding 3-argument shapes;ceil/flooron afloator a vector;abs/sqrt/sin/coson afloat; the conversionsfloat(x)/int(x); andvec2/vec3/vec4constructors as either a single-scalar splat or any scalar/vector mix whose components sum to the target width. - Swizzles — single components in any spelling (
xyzw,rgba,stpq), plus.xy,.zw,.xyzand.yzwfor reading. Only a single component can be assigned; a multi-component swizzle store is rejected, because the emitted vector types expose multi-swizzles as read-only accessors.
Explicitly not part of the grammar, each with its own error: while, do, switch, return, break, continue, discard, arrays and indexing, unary ~, the remaining compound assignments (%=, &=, |=, ^=, <<=, >>=), assignments nested inside expressions, initializing or assigning a pass as a value, and redeclaring COLOR or ctx. A blend/tev preset name cannot be used as a variable either.
Error behaviour
Unlike the software transpiler, which declines shaders outside its subset — they simply stay absent from its table — a fixed_function block is authored intent: anything outside the grammar is a hard error with the offending file and line, and it fails the whole GenerateAll.ps1 run, so a mistake surfaces on the dev machine instead of silently dropping a console effect. Common ones:
unknown pass field '.<name>' (fields: color, offset_color, screen_offset, blend, tev, luma_gain)unknown blend mode '<name>' (expected MATERIAL, ADD, OPAQUE or ALPHA), and the matchingunknown tev preset ...TINT_MIXorLUMA_RAMPreported as a GX-only capability — they need the programmable TEV combiner, so they are accepted only in a block targetinggxalone. A target list gets the reason spelled out:LUMA_RAMP is a GX-only capability - ... only available in a fixed_function(gx) block, not in one that also targets pspMODULATE_X2 has no GE equivalent - the PSP's texture environment has no combiner output scale, so it cannot be expressed for the psp target (...)— emitted for every block the psp target reaches: apspblock, a target list namingpsp(where the message reads for the psp target this block also names*, and fires even while another backend's header is being written), and a generic one- A builtin of the extended vocabulary reported as only available in a backend-specific block — generic blocks keep the portable quad-only core
vertex index 12 is outside the pvr strip builder's capacity of 8 vertices(the same message says 16 forgxandpsp; a target list is held to the smallest capacity among its targets and says so)pass fields cannot be read back (they are write-only descriptors),COLOR is read-only,a pass variable itself cannot be assigned (assign its fields)'while' is not part of the fixed_function grammar (statements: pass/local declarations, assignments, if/else, for, submit_quad)"pipeline <name>;" must be the only statement of the fixed_function blockandunknown pipeline "<name>" (known: tile_map_mesh, lighting_combine, line_strip_mesh)duplicate "void fixed_function(<target>)" block - the <target> target is already claimed by the block on line <N>(andduplicate "void fixed_function()" block - the generic block is already declared on line <N>) — one target belongs to one block, however it was spelledunknown fixed_function target "<name>" (expected pvr, gx, psp, a comma-separated list of them, or empty parentheses for the generic block),duplicate target "<name>" in the fixed_function target listandempty fixed_function target list entry(a trailing comma)
Worked examples
"WhiteMask.shader"— the minimal shape: one silhouette pass whose colour comes from the offset colour.PartialWhiteMask.shaderis the same effect split by capability, contrasting the offset-colour lift (one sharedvoid fixed_function(pvr, psp)block — neither console has an output scale, and both deliver an offset colour) against the combiner's x2 output scale (gx). The smallest example of the list form: two lines of pass code that used to exist twice."Outline.shader"— the ring is just a loop: a nestedforover the eight neighbours, each an offset silhouette guarded byhas_texel_size(), then the sprite itself. An idiom rather than a keyword, which is the whole point of the language being this small."Colorized.shader"— a runtime-dependent pass count on the no-combiner tiers (the multiplier split into up to three additive passes in one sharedvoid fixed_function(pvr, psp)block, since neither tier can carry a multiplier above 1.0) versus a singleMODULATE_X4pass on the GX."FrozenMask.shader"— the consoles reaching the same GLSLmix()differently, and the clearest illustration of why a target list is a capability statement: the PVR and the PSP share a block because neither can do per-texel arithmetic, so both settle for a constant ice tone, while thegxblock picks the tone per texel withLUMA_RAMPandluma_gain = 2.6, reproducing the shader's tone for every luminance."Transition.shader"— geometry synthesis: the iris fan built fromquad_origin()/quad_axis_x/y()and shaded strips, 32 segments in thepvrblock and 64 with a three-band eased edge in one sharedvoid fixed_function(gx, psp)block. The iris needs no combiner at all — only geometry and per-vertex colours — so what decides the split is the strip scratch: the GX and the GU both take the 10-vertex strip that walks all five radii in ONE draw, and the PVR's 8 vertices cannot hold it. It is also the one shader whose list pairsgxwithpsprather thanpvrwithpsp."Include/TexturedBackgroundWarp.inc"— the warp rebuild, shared by both background shaders and all three backends: the band geometry uses only extended vocabulary that every console implements the same way, so the include is literally the same code everywhere. Only the horizon-tint delivery differs, switched by aWARP_TINT_IN_VERTEX_COLORmacro the including block defines before the#include— set by thegxblock alone, which is why the other two consoles need nothing but a sharedvoid fixed_function(pvr, psp)block around the bare#include, and also the general idiom for specializing a shared include per backend.
Regenerating the committed headers
GenerateAll.ps1 in "Sources/Utilities/ShaderCompiler" runs the tool over every "Sources/Shaders/*.shader" and writes everything into "Sources/Shaders/Generated", in this order:
"ShaderCompilerTypes.h"— the shared reflection types (--emit-types)- One header per
.shaderfile —Default*.shader(the nCine default programs) go into thenCine::ShadersGennamespace, everything else intoJazz2::ShadersGen "ShadersGen.h"— the umbrella header including every generated program, plus the per-namespaceAllPrograms[]index arrays. Program symbols come from theprogramandbatcheddirectives, so a file with a batched twin contributes two entries"SwGeneratedShaders.h"— the software-renderer fragment functions (--emit-sw-generated)"PvrGeneratedEffects.h","GxGeneratedEffects.h"and"GuGeneratedEffects.h"— the console fixed-function effects (--emit-fixed-function pvr/gx/psp)
The typical workflow after editing a .shader file is therefore:
cd Sources\Utilities\ShaderCompiler
powershell .\GenerateAll.ps1
git add ..\..\Shaders ..\..\Shaders\GeneratedSPIR-V for the Vulkan backend is embedded when a glslangValidator can be found: an explicit -Glslang <path>, then VULKAN_SDK\Bin and VULKAN_SDK\Bin32, then PATH, then a Visual Studio-bundled copy, then a repo-local build-tree copy. glslang is a generation-time-only dependency: when it is unavailable a warning is printed and the SPIR-V fields are emitted as null, so the headers still build but the Vulkan backend is not buildable from them.
GenerateAll.ps1 -Check is the staleness guard: it generates into a temporary directory instead, byte-compares the result against the committed headers, removes the temporary directory and then either reports every stale file and exits non-zero, or confirms that everything is up to date — without ever modifying the tree. Missing and extra files count as stale too, and running -Check without glslang warns up front that every header with embedded SPIR-V will be reported stale. Run it after editing a shader, or in CI: the build itself never detects stale committed headers.
The tool also ships a test suite in "Sources/Utilities/ShaderCompiler/tests" — sample inputs with their exact --check dumps in "tests/expected", inputs that must fail to parse in "tests/errors", ESSL 100 fixtures in "tests/essl100", and tests/RunTests.ps1 to run everything including emitted-header shape assertions.
Editor support (Visual Studio Code)
"Sources/Utilities/VSCodeExtension" is a Visual Studio Code extension for the .shader language. Its point of difference from a generic GLSL mode is that it runs this tool: diagnostics are the compiler's own, obtained from ShaderCompiler <file> --check, so the squiggles in the editor and the errors from a regeneration cannot disagree. Around that it adds highlighting that knows the keyword directives and the fixed-function DSL, completion driven by the cursor's context, hovers that explain what the compiler does with each construct, an outline, #include navigation, and one-keystroke previews of the five source-to-source transforms.
It is plain JavaScript — no build step, no npm dependency, nothing to compile. The extension host runs "src/extension.js" directly.
Installing
Point the editor's extensions directory at the in-tree copy, then restart Visual Studio Code (or run Developer: Reload Window) so the new directory is scanned:
# Linux / macOS ln -sfn "$PWD/Sources/Utilities/VSCodeExtension" ~/.vscode/extensions/death-shader
rem Windows, from an elevated prompt (a directory junction needs no elevation on recent builds) mklink /D "%USERPROFILE%\.vscode\extensions\death-shader" "%CD%\Sources\Utilities\VSCodeExtension"
A symlink is preferable to a copy: the extension then tracks the repository, so a change to the language and a change to its editor support land together. To develop the extension itself, open "Sources/Utilities/VSCodeExtension" as a folder and press F5 — the bundled ".vscode/launch.json" starts an extension-host window with the repository root already open. For distribution outside the repository, npx @vscode/vsce package produces a .vsix; that is the only workflow that needs Node installed.
How it finds the tool
The executable is resolved in this order, the first hit winning:
- The
deathShader.compilerPathsetting, if set — absolute, or relative to the workspace folder (never relative to the editor's working directory). - Build outputs inside the workspace:
"Sources/Utilities/ShaderCompiler/<x64|ARM64EC|Win32>/<Release|Debug>/"(the MSBuild project's output, and whereGenerateAll.ps1expects it), then any CMake tree at the repository root whose directory name starts withbuildorcmake-build, then"out/build/<preset>/"for the Visual Studio CMake integration. ShaderCompileronPATH.
A status-bar item on the right names the executable in use, or warns when none was found; clicking it prints the resolved path. Without an executable the extension still works — it falls back to the checks in the second diagnostic layer — but nothing that needs to parse or transform a shader is available.
Highlighting, completion and navigation
The grammar is self-contained, so it does not depend on any other GLSL extension being installed. On top of ordinary GLSL it scopes the language's own vocabulary: the top-level directives, the uniform ... : hint list, the three entry points, the canvas built-ins, the compile-time stage macros, and the whole fixed-function DSL as its own region (pass, pipeline, the submit_* calls, the pass fields and their MATERIAL/ADD/... and MODULATE/LUMA_RAMP/... values, the optional context facilities). Constructs the language rejects are scoped as errors, so a colour theme paints them as mistakes before the compiler ever runs: fragColor, a #version line, an unsupported canvas built-in, an unknown render mode or uniform hint, and a stage macro used in #if defined(...) / #elif / #define / #undef.
Completion is chosen from the cursor's context rather than offered as one flat list:
| Where the cursor is | What is offered |
|---|---|
| Brace depth 0 | The directives, the entry points, #include, types, and the names this file declares |
After shader_type, render_mode or precision | Only that directive's legal values — nothing else |
After the : of a uniform | Only the seven uniform hints |
Inside the parentheses of void fixed_function(...) | Only pvr, gx, psp |
Inside a fixed_function body | The fixed-function statements, the submit_* calls, the pass fields with their enum values, the context facilities, and the small maths subset the transpiler accepts |
Inside vertex() or fragment() | GLSL built-ins and gl_*, the mode-appropriate canvas built-ins, and this file's uniforms, varyings, attributes, block members, structs, #defines, variant names and helper functions |
Inside an #include "..." | The sibling .inc / .shader files, walked directory by directory |
The mode-awareness is real, not cosmetic: the canvas built-ins are only offered under shader_type canvas_item (in custom mode TEXTURE is an ordinary user identifier, so offering it would be wrong), VERTEX only inside vertex(), and TEXTURE / PALETTE_OFFSET only outside it.
Hovering a directive, built-in, uniform hint, pass field or context facility shows what the compiler does with it — the same rules this page documents. Hovering a name the file declares shows its declaration line. Ctrl-clicking an #include path opens the file, go-to-definition works on any name declared in the document, and the outline (Ctrl+Shift+O) lists the programs, the batched twin, the variants, the uniform blocks, the uniforms with their texture units, the varyings, the attributes, the helper functions and the entry points with their fixed_function target lists.
Snippets cover the whole-file skeletons (shader-custom, shader-canvas) and every directive, entry point and fixed_function shape.
Diagnostics
Two independent layers, both on by default:
- The compiler.
ShaderCompiler <file> --checkis run over the document; it parses and reflects the input and writes nothing. Its stderr is parsed in all three shapes described under the command-line reference —"<file>:<line>: error: <message>","<file>: error: <message>"and a bare"error: <message>"— including Windows paths whose drive letter contains a colon. Anything the parser, the reflection or an emitter reports therefore reaches the editor verbatim. - Checks the extension can prove on its own, so a file still gets feedback with no executable around: a missing or duplicate
program, a missingvoid fragment()(orvoid vertex()in custom mode), a directive written beforeprogram,batchedwithoutshader_type canvas_item, a#versionline, a reference tofragColor, a stage macro in an illegal preprocessor form, areturn;inside a canvas-modevertex(), and an unsupported canvas built-in. Each one is a hard error this page documents. SetdeathShader.validate.builtinCheckstofalseto leave diagnostics entirely to the compiler.
The second layer knows when not to fire, which matters more than what it catches. Every structural check stands down for a file containing #include, because the includes are expanded textually before parsing and may legitimately carry the program directive or an entry point — exactly the shape "LightingMesh.shader" has. The canvas-only rules are skipped in custom mode, and a mention inside a comment never reports.
Validation runs on save, and on open. Setting deathShader.validate.run to onType validates while typing, debounced by deathShader.validate.delay. Since the compiler reads from disk, a dirty buffer is then written to a temporary copy first: next to the original when the document has #include lines (include paths resolve relative to the input file, so a copy elsewhere would not find them) and in the OS temporary directory otherwise. Those copies carry a ".vscode-death-shader-tmp." infix and are deleted immediately; leftovers from a killed session are swept up on activation. Nothing else in the tree is ever written.
Transform previews
The five inspection dumps are exposed as commands — right-click a .shader file, or use the command palette. All of them print to stdout and write nothing, so they are safe to run on a buffer at any time:
| Command | Runs |
|---|---|
| Death™ Shader: Show Reflection Dump | --check |
| Death™ Shader: Show HLSL Transform | --hlsl |
| Death™ Shader: Show Vulkan GLSL Transform | --vulkan |
| Death™ Shader: Show Cg Transform | --cg |
| Death™ Shader: Show ESSL 100 Transform | --essl100-check |
Each opens the transform in a preview beside the source, so the GLSL and what a given backend actually receives can be read side by side. Show Resolved ShaderCompiler Path and Validate Current File round out the command set.
Settings
| Setting | Default | Meaning |
|---|---|---|
deathShader.compilerPath | "" | Explicit path to the executable; empty auto-detects as described above |
deathShader.validate.enable | true | Report diagnostics at all |
deathShader.validate.run | onSave | onSave or onType |
deathShader.validate.delay | 400 | Debounce in milliseconds for onType |
deathShader.validate.builtinChecks | true | The executable-free checks of the second layer |
The extension declares itself unsupported in untrusted workspaces, since diagnostics and the previews execute a binary from the workspace.
Keeping it in step with the tool
.inc is registered for the language as well, since the shader includes in "Sources/Shaders/Include" use that extension. Such a file is treated as an include fragment: it is pasted textually into some .shader file, so it has no program directive, no entry points and no shader_type of its own. The structural checks therefore stand down for it (the ones that do not depend on file structure still apply), and because its shader mode cannot be known from the file alone, completion there offers the canvas built-ins regardless of mode instead of guessing.
The language vocabulary lives in exactly one place, "src/language.js", which both the completion and the hovers read. When this page gains a directive, a render_mode, a pass field or a context facility, add it there too — and to "syntaxes/deathshader.tmLanguage.json" if it needs its own colour. The test suite asserts the closed sets against what the parser accepts (the six render modes, the three fixed-function targets, the six pass fields, the four blend modes, the six tev presets, the three pipeline intrinsics), so a set that drifts out of step fails a test rather than silently offering a stale list.
node test/run-tests.js && node test/run-provider-tests.js
Both suites are dependency-free and also run under any other plain JS engine, gjs included. "run-tests.js" covers the text analysis — comment stripping, the document scan, the cursor contexts, the three diagnostic shapes, and every check together with the cases that must not fire. "run-provider-tests.js" drives the real editor providers against a mock vscode API wrapped in a proxy that throws on any member it does not define, so a mistyped API name fails in the test rather than in the editor.
The Cg dialect (PS Vita)
The PS Vita's sceGxm consumes compiled GXP shader binaries, and the VitaSDK ships no offline compiler for them — the only Cg compiler for the platform is libshacccg.suprx, a firmware module on the console itself. The tool therefore emits Cg source, which the backend compiles at load time through vitaShaRK, and that is what "CgGeneratedShaders.h" carries: two stage strings per program variant, looked up by the same program identity the fixed-function console tables use.
Cg is the same language family as HLSL — floatN, mul(), lerp / frac / ddx, TEXCOORD<i> interpolants — so it is emitted by the same emitter (ShaderCompiler::
- Both entry points are named
main, notVSMain/PSMain. - The system semantics are the fixed-function-era Cg set:
POSITIONfor the clip position,WPOSfor the fragment position andCOLORfor the colour output, in place ofSV_Position/SV_Target. - There is no
cbuffer: uniforms are plainuniformdeclarations, and a std140 block's members are hoisted to top-level uniforms. Samplers are combinedsampler2D/sampler3Dobjects carrying the GXMTEXUNIT<n>semantic and are read withtex2D()/tex2Dlod(). - There is no vertex-ID or instance-ID input at all — the GXM parameter semantics have no counterpart — so the same
VertexIdRewritethe ESSL 100 profile uses turns the engine'sgl_VertexIDquad synthesis into reads of theaQuadCorner/aInstanceIndexattributes. A stage still referencing either built-in after that rewrite is rejected with a diagnostic rather than mis-emitted. - A batched shader's
BATCH_SIZEis baked into the emitted source as a plain#define, because a Cg source is compiled as one string with no place to inject a define ahead of it. The backend rewrites that number when the runtime settles on a different batch size.
Platform notes
- OpenGL|ES 2.0 has neither uniform buffer objects nor
gl_VertexID, and the shared sprite template uses both, so the ESSL 100 lowering rewrites std140 blocks into loose uniforms or a uniform struct array and replaces the quad corner and batched instance index with theaQuadCorner/aInstanceIndexvertex attributes. The profile is selected withNCINE_RHI_GL_PROFILE=ES2, compiles with"#version 100", and prefers the baked*100sources; a construct outside the emitter's subset leaves them null. - Direct3D 11 consumes the baked HLSL —
VSMain/PSMainentry points, std140 blocks as cbuffers, separateTexture2D+SamplerStateobjects andmul()-based matrix algebra. - Vulkan builds pipelines straight from the embedded SPIR-V and its descriptor-set layout from the same reflection.
- The software renderer is the only tier built with
SOFTWARE_RENDERERdefined, and the only one that can decline a shader without failing generation. - Dreamcast, Wii/GameCube and PlayStation Portable include their aggregate effect header from the backend's device file under
WITH_RHI_PVR/WITH_RHI_GX/WITH_RHI_GU, and resolve their table entry from the program identity at link time. - PlayStation Vita is the one console with real shaders: under
WITH_RHI_GXMit resolves its Cg stage sources from the same kind of identity lookup and compiles them on the console, see above.
Console content is fully preprocessed — see AssetPacker for the asset side of the same principle — which is why runtime-compiled shaders are a desktop-tier feature: there is no C++ JIT on a console, so a shader that arrives at runtime has no fixed-function effect to run — and on the Vita it has no Cg source, which is the same restriction wearing a different hat. See Building for consoles for how those backends are built and run.
Pitfalls
- Committed generated headers are never rebuilt by the build. Editing a
.shaderfile does nothing untilGenerateAll.ps1is re-run and the regenerated headers are committed; use-Checkto catch it. - Do not edit anything in
"Sources/Shaders/Generated"by hand —-Checktreats a hand-edited header as stale, and the next regeneration overwrites it. That includes the aggregate console headers: the effect they contain belongs in the shader file. - SPIR-V requires glslangValidator at generation time, and the glslang and
D3DCompileintegrations are Windows-only, so a full regeneration has to happen on Windows. A-Checkrun elsewhere disagrees with headers that were generated with SPIR-V. - A layout change of
"ShaderCompilerTypes.h"invalidates all committed generated headers at once — one regeneration commit, guarded by-Check. - An error in a
fixed_functionblock fails the whole run by design, while a shader the software transpiler cannot handle is merely listed as declined. Read the summary lines: a silently missing console effect is a missing block, not a failure. - The
--emit-fixed-functionmode never reflects GLSL, so a block is validated even when the GLSL around it is not — and the other way round:--checksays nothing about a block. - A misspelled directive is not diagnosed. Unknown top-level keywords fall through into the shared globals and reach GLSL verbatim, so
render_modee blend_mix;fails in the GLSL compiler rather than in the tool. Likewise, text after the;of avarying,attributeorrender_modestatement is silently discarded. - Function-like macros are recorded but never expanded by the reflection preprocessor; there can be only one
BATCH_SIZEarray per block and it must be the last member; struct-typed loose uniforms, sampler arrays, multi-dimensional arrays and array vertex attributes are rejected with an error.