Authored Vulkan material programs

September 25, 2026 · View on GitHub

Status: native drawing is implemented for an initial subset of authored GLSL ambient and per-light stages. Unknown program names now load and compile their actual source pair. Stock-name overrides also use their actual source; an embedded stock implementation requires a reviewed source fingerprint or a canonical name with no source files. Vulkan remains experimental, and the full custom-program requirement is still open.

Compiler contract

src/renderer/materialprogram/GLSLCompiler.cpp accepts a vertex/fragment source pair and explicit, case-sensitive material parameter and texture bindings. It uses the pinned glslang 15.1.0 compiler in process, through a static library built by the project's native Meson subproject. It does not invoke an installed SDK executable, perform file access, or create GPU resources. Source inputs are limited to one MiB each; empty and embedded-NUL inputs are rejected. Compilation is serialized around the library's process lifetime and independent callers can compile safely.

The original OpenGL GLSL pair is parsed and linked before translation. This is necessary because the compatibility layer initializes per-invocation globals from uniform storage; translating first could accidentally legalize an authored write to a uniform. The glslang preprocessor expands macros and conditionals. The translator operates on tokens, preserving diagnostic line records and function-local variable shadowing. Generated Vulkan shaders are linked together before emitting SPIR-V 1.3 for Vulkan 1.1. Failed compilation clears both output modules and returns a diagnostic, with the shader filename for source failures.

The initial language subset covers GLSL 1.10/1.20 scalar/vector float uniforms, 2D/cube samplers, named varyings, the engine's explicitly bound vertex attribute names, common legacy vertex/color/texture-coordinate inputs, model-view, projection, normal and texture matrices, fragment color and coordinates, and the supported 2D/cube explicit-LOD/gradient functions. Material names are not a shader-family whitelist. Uniform and interface arrays, authored matrix/int/bool uniforms, other sampler types, newer GLSL versions and other extensions remain unsupported and receive diagnostics. Unrecognized legacy built-ins must pass the Vulkan compiler; they are not silently replaced with dummy values.

The parameter capacity is the engine's 32 slots; the texture capacity is eight. Active scalar/vector parameters must match the material's upload width. The runtime draw interface is:

ResourceBindingLayout
Per-draw dataSet 0, binding 01,312-byte std140 block
Authored texturesSet 1, bindings 0–7Separate combined image samplers
Vertex inputsLocations 0–5Position, color, normal, tangent, bitangent, UV

The uniform block contains 32 parameter vectors, model-view and projection matrices, eight texture matrices, stage color, four draw controls and two CPU-composed model-view-projection matrices. Matrices are column-major. Authored matrix operations retain GL clip coordinates; the generated vertex entry point converts clip depth to Vulkan's range. Unmodified ftransform() or legacy MVP positions use the same CPU-converted matrix as depth fill, preserving equal-depth lighting coverage. Changed authored positions keep the ordinary clip conversion. Window Y and dFdy select the target's orientation through a pipeline specialization constant. In the authored GLSL path, vertex color arrays are exposed directly, matching draw_common.cpp; the wrapper does not invent stage tint or inverse modulation for those arrays. The first native comparison covers texture coordinates, model-view/projection, authored vertex offsets, fragment coordinates and derivatives in the ordinary display path. Other target, color and geometry combinations remain unqualified.

Runtime ownership

OpenGL and Vulkan share the source-pair search order in MaterialProgramSource.cpp. Vulkan passes the complete file lengths to the compiler, preserving embedded-NUL rejection. Shader identity includes the program name, parameter names/order/semantics and texture names/order. Numeric values and image selection remain per draw. reloadGLSLprograms invalidates the authored source lookup on both backends; reloadARBprograms also performs this GLSL reload. Vulkan reuses unchanged valid or invalid source pairs before checking its version limit. Changed versions stay alive until device shutdown so recorded draws cannot reference a destroyed module or a reused pipeline identity.

Native stock selection uses material_program_sources.json and the generated vk_MaterialSourceSignatures.h. Both source files must match one reviewed pair; only CRLF line endings are normalized. The table contains hashes, not retail shader text. tools/build/vulkan_material_sources.py --check is registered in the native Meson tests and script CI. It compares the generated table with the reviewed manifest and never automatically approves changed shader source. Different directories with the same basename are authored programs. Modified, empty and incomplete pairs cannot silently select a stock implementation. Some retail guide families have no GLSL source files; their canonical names retain the existing built-in default only while every candidate source is absent. Source selection is cached until an explicit shader reload or device shutdown. A complete pair retains the shared filesystem search order.

OpenGL reload invalidates parsed material stages and the engine's material GLSL stages. Failed attempts are cached in the current GL context, so a broken file does not compile again on every draw. Explicit reload or a new context retries it. Neither backend needs a game restart to load a repaired supported shader.

Each frame slot owns a descriptor pool reset only after its fence completes. The larger uniform block uses the existing frame ring with the device's uniform alignment. All eight texture descriptors use the existing image residency and transition path. Shader modules, layouts, pools and CPU records are destroyed with the device, after the executor destroys its pipelines. GL handle fields are never used to store Vulkan handles.

Limits are explicit: 256 retained program versions, 32 MiB of retained compiler source/module data, 2,048 authored draws per frame slot, and the executor's shared 128-entry material pipeline cache. Allocation, type and unsupported binding failures diagnose and skip the affected stage. Exhaustion/recovery drills are still required; these bounds are not a performance claim.

Authored lighting

Supported customLighting GLSL stages now execute once per contributing light using their actual shader pair. Per-draw bindings include local light/view origins, projected-light and falloff planes, bump/diffuse/specular texture rows, diffuse/specular colors, vertex-color modulation, model/projection rows and the identity color matrix. Light projection/falloff and other engine texture semantics resolve from the current interaction. Stage conditions, colors and texture transforms remain material-controlled. Draws accumulate with ONE/ONE blending and inherit the receiver's geometry, depth, cull, bias and stencil state.

Arbitrary authored shaders do not have the engine's native shadow-map sampler contract. When mapped shadows are requested, only affected receivers run through a separate stencil-filtered interaction walk; other surfaces retain their mapped shadows. This matches OpenGL's retail stencil caster policy and does not add support for expanded map-only casters. The front end conservatively retains volumes for custom or unresolved interactions before submitting any receiver. The cost of that conservative retention still needs performance qualification. The two verified native lighting families keep their existing mapped path.

Explicit linear/nearest image filters now select the base mip level even when an image already has a mip chain. This matches OpenGL's sampler behavior across image reload. The lighting investigation also repaired OpenGL's material texture table: unused declared array slots now receive valid material images instead of retaining a comparison-enabled shadow texture from a previous pass.

Validation and remaining work

The native openq4-material-compiler test covers valid paired programs, parameters at the capacity boundary, 2D/cube textures, named/legacy attribute aliases, normal transforms, macros, local variable shadowing, derivatives, explicit LOD, deterministic recompilation and concurrent callers. Negative cases cover syntax and link failures, uniform writes, invalid/duplicate bindings, unsupported interfaces and source-size constraints. An independent spirv-val check validates the emitted modules. These checks establish compilation and module validity; they are not image, performance, or gameplay qualification.

The v50 Windows native-Meson run passed all 31 cases and the registered Meson test. The v51 suite adds two invalid parameter-width cases and passes 33 cases. Independent validation passes all 24 emitted modules and verifies the uniform member offsets, matching vertex/fragment interfaces, boundary texture slots and deterministic recompilation. The latest compiler evidence is retained in .tmp/vulkan-gap-closure/authored-glsl/compiler-qualification.json; the prior foundation evidence remains in the material-programs directory. All 2,978 extracted upstream files and both overlay files were compared with their pinned inputs. Other operating systems have not yet run this compiler suite. The v51 runtime links the compiler into the Vulkan module; it requires the matching staged engine and renderer modules. The prior PBR cutout fix is retained, and its separate filtering discrepancy remains open.

The expanded v51 scene uses two independently compiled material programs and engine screenshot captures in hidden, windowed, input-disabled gameplay. The ten-control v51a run passes GL/Vulkan comparisons within one display level; image reload, partial restart and full restart reproduce each backend's base image exactly. Its unchanged-source shader control did not detect that Vulkan had not registered the reload command; v52 adds actual live-edit coverage. The reusable harness is tools/tests/renderer_vulkan_material_programs.py; reports remain under .tmp/vulkan-gap-closure/authored-glsl/. This is local image evidence, not stock-map, performance, long-session or platform promotion evidence.

The capacity scene exposed a separate GL reference bug: ambient GLSL stages linked the named tangent/bitangent/normal attributes to lanes 9/10/11 without enabling and supplying those arrays. Those stages now bind the surface basis and legacy normal array around the draw. The capacity fixture uses all three named basis vectors to choose cube faces; a constant-color cube is a separate control. This preserves a meaningful reference instead of reproducing stale attribute values in Vulkan.

After that repair, all twenty v51d and final v51e controls pass. The 32-parameter/eight-texture program, the changed last parameter and each independently changed texture slot match GL pixel for pixel. The remaining controls differ by at most one 8-bit display level, within the unchanged limit of two. Both backends show more than 29,000 changed pixels for each binding control and exact recovery images. The tests cover named basis attributes, scalar/vec2/vec3/vec4 parameters and 2D/cube samplers. These results do not establish untested language, blend or target combinations. Failed intermediate reports remain retained with their diagnostics. The final staged build also passes the render-target/startup, HDR and temporal motion GPU regression groups with zero tracked warnings. Runtime, sources, compiler outputs and reports are bound by .tmp/vulkan-gap-closure/authored-glsl/checkpoint-v51-authored-glsl.json.

The v52 live-edit suite, tools/tests/renderer_vulkan_material_reload.py, adds sixteen controls per backend. It exercises a stock basename under a different directory, a canonical stock source replacement, changed fragment and vertex source, syntax failure and repair, a fragment-only canonical source pair and repair, both reload commands, and partial/full video restart. All sixteen GL/Vulkan pairs differ by at most one display level; each backend's recovery and deliberate failure images match their references exactly. The captures also require the three unchanged SMAA sources to select verified native code. The two deliberate failures each produce exactly one primary diagnostic before repair, including a subsequent still-invalid capture. Expected compiler messages are accepted only inside explicit bounds; unrelated warnings, API errors and unknown commands still fail the run. Raw logs remain retained.

This suite found that the earlier Vulkan reload functions had no registered commands: registration was inside OpenGL initialization. Both commands now register in shared renderer initialization. The first failed Vulkan run is retained, and the common laboratory now rejects unknown commands. The initial GL diagnostic parser also needed to strip engine color codes, and the Vulkan parser needed the specific follow-on compiler messages for the deliberately undefined identifier. These parser failures remain failed reports. Accepted source-edit evidence is gl-v52b, vk-v52c and compare-v52b under .tmp/vulkan-gap-closure/material-reload/.

The final staged runtime also passes 35 OpenGL HDR/bloom controls, including exact full-frame restoration after both GLSL reload commands. The Vulkan render-target/startup, HDR and temporal-motion GPU groups pass without tracked warnings. Both registered native tests pass; the 33-case compiler output is byte-identical to all 24 independently validated v51 modules. The implementation and retained evidence are bound by .tmp/vulkan-gap-closure/material-reload/checkpoint-v52-material-reload.json.

The v53 fixture adds 26 authored-lighting controls with 27 semantic bindings, point/projected lights, changed projection/falloff textures, material transforms, vertex colors, conditions, multiple stages/lights, rotation and shadow/recovery controls. Both v53e backend runs pass independently with exact image/shader/video recovery and exact mapped-to-stencil equivalence on the custom receiver. Every receiver comparison stays within one display level, including exact shadowed receiver output. Projected-shadow full frames differ by at most one level. The strict full-frame comparison still fails for three point-stencil background pixels (maximum 30 levels) and nineteen point-map pixels (maximum five). The receiver analysis only locates these failures; the original full-frame gate remains failed. Reports and engine captures are retained under .tmp/vulkan-gap-closure/authored-lighting/.

Development runs exposed fixture projection/removal errors, canonical transform rounding holes, an overly restrictive stencil-admission check, the OpenGL sampler-table conflict and a one-pixel Vulkan image-reload change. Those reports remain failed evidence. The v53e sampler correction closes the last exact-recovery failure without changing the threshold. Compiler qualification passes all 33 native cases and independently validates 24 newly generated modules with the expanded uniform block; see authored-lighting/compiler-qualification.json.

The final v53e runtime also passes twenty ordinary authored-material comparisons within one display level, with exact recovery, all 35 OpenGL HDR/bloom controls and the three Vulkan render-target/startup, HDR and temporal-motion GPU groups. These runs report no tracked API/engine diagnostics. Source/runtime identity, the passing scoped checks and the failed full-frame comparison are retained in .tmp/vulkan-gap-closure/authored-lighting/checkpoint-v53-authored-lighting.json.

The v56 point-shadow correction retains all 26 independent lighting controls and twenty ordinary material comparisons. Its complete mapped-shadow image now stays within two display levels of the unchanged GL reference. Three point-stencil background pixels still fail, so the full lighting comparison remains failed overall. This follow-up does not extend authored-shader admission or weaken the image gate.

Remaining work includes:

  • Extend source-selection qualification to additional retail/mod source pairs, filesystem overlays, and long repeated-edit sessions.
  • Qualify additional custom-lighting combinations, including mixed stages, translucent receivers, alpha coverage, animated geometry and resource failure. The generic evaluator now handles the per-light semantics above in addition to numeric registers, view origin, current render viewport/scale and supported post-process size/color-space bindings. Full-scene shadow parity remains open.
  • Implement sampling from flipped render textures; current generic draws reject those images explicitly. Qualify offscreen/HDR, texture generation, alpha and blend state, vertex colors, decals and animated geometry.
  • Extend the GLSL language subset and qualify descriptor/pipeline/uniform/cache exhaustion, repeated source edits and device-failure recovery.
  • Implement and qualify authored ARB assembly programs. The GLSL compatibility compiler is not an ARB assembly translator.

The broader HDR/PBR/shadow, filtering, performance, CI, hardware and release requirements in the gap-closure plan remain in force.

Dependency provenance

glslang 15.1.0 is pinned by archive SHA-256 in subprojects/glslang.wrap. Its BSD, MIT, Apache-2.0, GPLv3 with Bison exception and permissive preprocessor notices were reviewed for this GPLv3 repository before incorporation. The native Meson overlay selects the upstream compiler/SPIR-V/resource-limit sources, without changing their source or notices. Optional HLSL, SPIRV-Tools, remapper and upstream test dependencies are excluded. The complete upstream license is retained in docs/licenses/glslang-LICENSE.txt and included in the staging manifest under licenses/; the README credits the original project.