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.

BackendArtifact it consumesProduced by
OpenGL 3.3, OpenGL|ES 3.0, WebGL 2.0VsSource / 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 headerprimary mode
Direct3D 11HlslVsSource / HlslFsSource — Shader Model 4/5 HLSL baked into the same headerprimary mode
VulkanVkVsSpirv / VkFsSpirv — SPIR-V words compiled offline through glslangValidatorprimary 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::RuntimeProgram) only ever produce GLSL plus reflection. Under the ES2 profile the engine additionally runs the very same ESSL 100 emitter at load time, so runtime shaders get the same lowering; they have no HLSL, no SPIR-V and no fixed-function effect, which is why external shaders are unavailable on the consoles.

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 optionMeaning
-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
--checkHuman-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 essl100ESSL 100 (OpenGL|ES 2.0) transform of every variant's stage sources. essl100 is the only accepted --target value
--hlslHLSL (Shader Model 4/5) transform of every stage
--cgCg transform of every stage, in the dialect the PS Vita's sceGxm backend compiles (the same emitter as --hlsl, see below)
--vulkanVulkan 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

DirectiveMeaning
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

ElementMeaning
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 elseGlobal 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-inVertex stageFragment stage
VERTEXsprite-local position in pixels, writable
UVtexture coordinates, writablesubstituted with vTexCoords
COLORinstance color, writableenters as the instance color, and is the output
PALETTE_OFFSETpalette row of the instance, writablesubstituted with vPaletteOffset
TEXTUREsubstituted 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-generated path 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 approximates pow(distance, 1.5) and drops the per-pixel star field. Wrapped around global-scope varying declarations** 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 but varying declarations (and no nested directives). "tests/SwVarying.shader" is the worked example — vPos for the shader backends, vRect for the software one.
  • #ifdef <VARIANT> on a variant name resolves per variant, in the GLSL stages and inside a fixed_function block 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 / _Fs for the base variant, <Program>_<VARIANT>_Vs / _Fs for 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 constexpr arrays of plain structs (Uniform, BlockMember, UniformBlock, TextureBinding, Attribute), tied together by ProgramVariant and Program

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::ConsoleFixedFunction for the emitter.

Block selection and what gets emitted

A block's parentheses hold its target list:

SpellingServes
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 reachable p.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 is true on the PVR and the PSP, which lift the sprite with an offset colour, and false on the GX, which uses the combiner's x2 output scale instead.
  • requirements — a FixedFunctionRequirements bitmask of the optional EffectContext facilities the function can ever call: NeedsTexelStep (texel_size() / has_texel_size()), NeedsUniforms (has_uniform() / uniform_vec2/vec4()), NeedsStripBuilder (strip_*() / submit_strip[_shaded]()) and NeedsQuadAxes (quad_origin() / quad_axis_x/y()). The backends' Dispatch gates 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 nameFixedFunctionIntrinsicStageDeclared by
tile_map_meshTileMapMesha 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_combineLightingCombinethe viewport compositor — the direct-tier CPU-lightmap lighting hookCombine.shader, CombineWithWater.shader, CombineWithWaterLow.shader
line_strip_meshLineStripMeshvertex-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.

FieldTypeMeaning
p.colorvec4per-vertex colour of the pass (the PVR argb, the GX raster colour, the GE vertex colour)
p.offset_colorvec3post-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_offsetvec2displacement of the whole quad in the quad's own coordinate space (the Outline ring taps)
p.luma_gainfloathow much LUMA_RAMP amplifies the texel's luminance before saturating it; ignored by every other preset
p.blendenumblend override for this pass
p.tevenumGX combiner preset — portable intent, the PVR always modulates and the GE only knows MODULATE and SILHOUETTE
p.blendBlendModeHardware
MATERIALMaterialwhatever the material configured (the default)
ADDAdditiveadditive 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
OPAQUEOpaqueONE + ZERO
ALPHAAlphaplain 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.tevTevPresetMeaning
MODULATEModulatetexture * vertex colour (the default)
SILHOUETTESilhouettevertex colour where the texture has alpha (flat masks, shadows, glows)
MODULATE_X2ModulateX2modulate 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_X4ModulateX4modulate with the combiner's x4 output scale. Not on the PSP, same reason
TINT_MIXTintMixmix(texel, colour, alpha) with an opaque result, one combiner stage. GX only — needs a block targeting gx and nothing else
LUMA_RAMPLumaRampsilhouette 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-inTypeMeaningGeneric block?
COLORvec4the instance colour of the draw being dispatched, exactly the shader's COLOR input. Read-only, and cannot be redeclaredyes
texel_size()vec2displacement 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()boolwhether that step is derivable at all (a zero texRect has no scale) — blocks guard their texel_size() uses with ityes
submit_quad(p)statementsubmits one pass over the current instance's quadyes
quad_origin()vec2pre-clip raster position of the sprite's (0,0) cornerno — pvr/gx/psp, alone or in a list
quad_axis_x(), quad_axis_y()vec2pre-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 itno — pvr/gx/psp, alone or in a list
has_uniform(uName)boolwhether the program resolved that uniform. An unresolved name loads zeros, so blocks guard with this exactly like the code they replaced null-checked its pointersno — pvr/gx/psp, alone or in a list
uniform_vec2(uName), uniform_vec4(uName)vec2, vec4the program's resolved uniforms by name, through the backend's existing ResolveUniform machinery. The argument is an identifier, not a string literalno — pvr/gx/psp, alone or in a list
strip_position(i, <vec2>)statementstrip-builder vertex positionno — pvr/gx/psp, alone or in a list
strip_uv(i, <vec2>)statementstrip-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>)statementstrip-builder per-vertex colourno — pvr/gx/psp, alone or in a list
submit_strip(p, count)statementsubmits the first count strip vertices textured, under the pass's flat colourno — pvr/gx/psp, alone or in a list
submit_strip_shaded(p, count)statementsubmits 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 UVsno — 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.

RuleOn 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_RAMPallowed only in a block targeting gx and nothing else. void fixed_function(gx, psp) is rejected — the GE has no combiner
MODULATE_X2 / MODULATE_X4rejected 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 capacitythe 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), pass declarations, plain and compound assignment (=, +=, -=, *=, /=), prefix and postfix ++ / -- on an int variable, if / else, C-style for whose init declares or assigns an int counter and whose condition is a bool, 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; % on int only; < > <= >= on numeric scalars; == and != on numeric scalars or bools; && || ^^ on bools; unary - on any numeric scalar or vector, unary ! on a bool; and the ternary ?:, whose condition must be bool and whose branches must have the same type.
  • Functionsmin / max taking (float, float), (int, int), (vecN, vecN) or (vecN, float); clamp and mix in the corresponding 3-argument shapes; ceil / floor on a float or a vector; abs / sqrt / sin / cos on a float; the conversions float(x) / int(x); and vec2 / vec3 / vec4 constructors 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, .xyz and .yzw for 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 matching unknown tev preset ...
  • TINT_MIX or LUMA_RAMP reported as a GX-only capability — they need the programmable TEV combiner, so they are accepted only in a block targeting gx alone. 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 psp
  • MODULATE_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: a psp block, a target list naming psp (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 for gx and psp; 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 block and unknown 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> (and duplicate "void fixed_function()" block - the generic block is already declared on line <N>) — one target belongs to one block, however it was spelled
  • unknown 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 list and empty 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.shader is the same effect split by capability, contrasting the offset-colour lift (one shared void 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 nested for over the eight neighbours, each an offset silhouette guarded by has_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 shared void fixed_function(pvr, psp) block, since neither tier can carry a multiplier above 1.0) versus a single MODULATE_X4 pass on the GX.
  • "FrozenMask.shader" — the consoles reaching the same GLSL mix() 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 the gx block picks the tone per texel with LUMA_RAMP and luma_gain = 2.6, reproducing the shader's tone for every luminance.
  • "Transition.shader" — geometry synthesis: the iris fan built from quad_origin() / quad_axis_x/y() and shaded strips, 32 segments in the pvr block and 64 with a three-band eased edge in one shared void 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 pairs gx with psp rather than pvr with psp.
  • "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 a WARP_TINT_IN_VERTEX_COLOR macro the including block defines before the #include — set by the gx block alone, which is why the other two consoles need nothing but a shared void 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:

  1. "ShaderCompilerTypes.h" — the shared reflection types (--emit-types)
  2. One header per .shader file — Default*.shader (the nCine default programs) go into the nCine::ShadersGen namespace, everything else into Jazz2::ShadersGen
  3. "ShadersGen.h" — the umbrella header including every generated program, plus the per-namespace AllPrograms[] index arrays. Program symbols come from the program and batched directives, so a file with a batched twin contributes two entries
  4. "SwGeneratedShaders.h" — the software-renderer fragment functions (--emit-sw-generated)
  5. "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\Generated

SPIR-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:

  1. The deathShader.compilerPath setting, if set — absolute, or relative to the workspace folder (never relative to the editor's working directory).
  2. Build outputs inside the workspace: "Sources/Utilities/ShaderCompiler/<x64|ARM64EC|Win32>/<Release|Debug>/" (the MSBuild project's output, and where GenerateAll.ps1 expects it), then any CMake tree at the repository root whose directory name starts with build or cmake-build, then "out/build/<preset>/" for the Visual Studio CMake integration.
  3. ShaderCompiler on PATH.

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 isWhat is offered
Brace depth 0The directives, the entry points, #include, types, and the names this file declares
After shader_type, render_mode or precisionOnly that directive's legal values — nothing else
After the : of a uniformOnly the seven uniform hints
Inside the parentheses of void fixed_function(...)Only pvr, gx, psp
Inside a fixed_function bodyThe 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:

  1. The compiler. ShaderCompiler <file> --check is 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.
  2. Checks the extension can prove on its own, so a file still gets feedback with no executable around: a missing or duplicate program, a missing void fragment() (or void vertex() in custom mode), a directive written before program, batched without shader_type canvas_item, a #version line, a reference to fragColor, a stage macro in an illegal preprocessor form, a return; inside a canvas-mode vertex(), and an unsupported canvas built-in. Each one is a hard error this page documents. Set deathShader.validate.builtinChecks to false to 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:

CommandRuns
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

SettingDefaultMeaning
deathShader.compilerPath""Explicit path to the executable; empty auto-detects as described above
deathShader.validate.enabletrueReport diagnostics at all
deathShader.validate.runonSaveonSave or onType
deathShader.validate.delay400Debounce in milliseconds for onType
deathShader.validate.builtinCheckstrueThe 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::HlslEmitter, selected with Dialect::Cg) rather than one of its own. The deltas are these:

  • Both entry points are named main, not VSMain / PSMain.
  • The system semantics are the fixed-function-era Cg set: POSITION for the clip position, WPOS for the fragment position and COLOR for the colour output, in place of SV_Position / SV_Target.
  • There is no cbuffer: uniforms are plain uniform declarations, and a std140 block's members are hoisted to top-level uniforms. Samplers are combined sampler2D / sampler3D objects carrying the GXM TEXUNIT<n> semantic and are read with tex2D() / tex2Dlod().
  • There is no vertex-ID or instance-ID input at all — the GXM parameter semantics have no counterpart — so the same VertexIdRewrite the ESSL 100 profile uses turns the engine's gl_VertexID quad synthesis into reads of the aQuadCorner / aInstanceIndex attributes. A stage still referencing either built-in after that rewrite is rejected with a diagnostic rather than mis-emitted.
  • A batched shader's BATCH_SIZE is 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 the aQuadCorner / aInstanceIndex vertex attributes. The profile is selected with NCINE_RHI_GL_PROFILE=ES2, compiles with "#version 100", and prefers the baked *100 sources; a construct outside the emitter's subset leaves them null.
  • Direct3D 11 consumes the baked HLSL — VSMain / PSMain entry points, std140 blocks as cbuffers, separate Texture2D + SamplerState objects and mul()-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_RENDERER defined, 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_GXM it 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 .shader file does nothing until GenerateAll.ps1 is re-run and the regenerated headers are committed; use -Check to catch it.
  • Do not edit anything in "Sources/Shaders/Generated" by hand-Check treats 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 D3DCompile integrations are Windows-only, so a full regeneration has to happen on Windows. A -Check run 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_function block 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-function mode never reflects GLSL, so a block is validated even when the GLSL around it is not — and the other way round: --check says 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 a varying, attribute or render_mode statement is silently discarded.
  • Function-like macros are recorded but never expanded by the reflection preprocessor; there can be only one BATCH_SIZE array 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.