vcti-shader-compiler
Offline Slang → GLSL ES 3.00 shader compiler.
Overview
vcti-shader-compiler lets you write a shader once, in Slang, and get
validated shader text for the graphics API that has to run it. It drives a
pinned external toolchain (slangc → SPIR-V → SPIRV-Cross → glslang) at build
time, derives each shader's uniform layout from the Slang compiler's own
reflection data, repairs the defects that cross-compilation introduces, and can
execute a compiled shader headlessly so you can test its output in Python.
Nothing it produces needs a shader compiler at runtime.
GLSL ES 3.00 is the only target implemented. Slang itself can emit WGSL, and adding it is mostly a matter of removing steps rather than adding them — docs/wgsl.md records what was tried, what it would change, and the one question worth settling first.
Why author shaders this way
Shader source is awkward to work with in three ways, whatever you happen to be rendering, and they show up even if you only ever target one graphics API:
- There is no way to share code. GLSL has no modules and no
import— a shader is one flat translation unit. Anything two shaders both need gets copied, or assembled at runtime by string concatenation and#defineswitches. That is how most large shader codebases end up built, and it is why they are hard to change safely. - Nothing checks it until a GPU does. A typo, a varying that does not match between stages, a uniform spelled two ways — none of it surfaces until a driver compiles the shader, and then the symptom is a blank frame or a subtly wrong image rather than an error naming the line.
- It is hard to test. Confirming that a shader computes the value you meant usually means rendering something and looking at it. There is no natural way to assert on the numbers.
A fourth reason appears later in a project's life: the API you compile for is not necessarily the one you will always target. A renderer written against WebGL2 wants GLSL ES 3.00; moving to WebGPU means WGSL; adding a native or a server-side path means something else again. That is a rewrite of the shader library unless the source was written independently of the target.
This package's answer to all four:
- Slang as the source language. An
open-source, Khronos-hosted shading language with HLSL-like syntax. It has
real modules and
import, so shader code composes like ordinary code; and it compiles to SPIR-V, the portable shader IR (Intermediate Representation), from which the same source can be emitted as GLSL, WGSL, MSL, or HLSL. Write the logic once, independently of the target. - Compilation offline, on a build machine. The shader is generated and type-checked before anything ships, so a mistake fails your build rather than your users' frame.
- Execution from Python.
render_readbackruns the compiled shader headlessly and hands back what the GPU computed as a NumPy array, so shader math can be asserted on in an ordinary test.
The package compiles shaders and nothing else — it has no opinion about what a shader computes, no catalogue of shader kinds, and no idea where your Slang modules live. So none of this is specific to a subject area: it applies to any shader you would rather write once, check before shipping, and test like normal code. VCollab uses it, for example, to build the shaders behind its CAE viewers, but the package knows nothing about that.
How it works
Two passes over the same source, both driven by slangc:
┌─ slangc ─→ SPIR-V ─→ spirv-cross ─→ GLSL ES 3.00 ─→ glslang ✓
your.slang ───┤
└─ slangc ─→ reflection JSON ─→ uniform layouts + attributes
SPIR-V sits in the middle because it is the interchange format both halves of
the toolchain speak: Slang emits it, SPIRV-Cross consumes it. glslang then
parses and type-checks the emitted GLSL, so invalid output never reaches
whatever consumes it.
Cross-compiled output is corrected rather than trusted, because each of these defects is invisible until far downstream — or, in one case, stops the build with a diagnostic about an extension nobody asked for:
- Inter-stage varyings are renamed. GLSL ES 3.00 links varyings by name
and forbids
layout(location)on them, but separately compiled stages get unrelated generated names — so the pair silently fails to link in the consumer. Both sides are forced to a sharedv{location}. halfbecomesmediump. Slang'shalfcross-compiles to fp16 types behind two desktop vendor extensions, which no WebGL2 driver accepts and the validator rejects — sohalfwould otherwise fail the build outright. Those types are rewritten to explicitlymediumpfp32, the ES spelling of the same intent, lowering exactly the varyings and locals the source asked to lower.halfin a uniform block is refused instead, because Slang packs it as two bytes and widening it would move every later member off its reflected offset.- Every stage declares its float precision,
highpby default. SPIRV-Cross emitsprecision mediump float;for a fragment stage — as little as 10 bits of mantissa, fine for colours but lossy for measured data or large coordinates — and emits nothing at all for a vertex stage, where ES 3.00's own default ishighp. Left alone, one source computes at two precisions, and the symptom is quietly wrong pixels rather than an error. Passprecision=to choose something else; see docs/precision.md for what lowering it reaches.
Uniform offsets come from slangc's reflection rather than from hand-computed std140 rules, so packing stays correct when a member is added.
Composing shaders from modules
Because sources are Slang modules, a pipeline is assembled by importing rather than by pasting text together:
import lighting; // a shared lighting model
import colormap; // mapping a value to a colour
You tell the compiler where those modules live by passing include_dirs, which
become slangc -I search paths. That is what lets one shader pull in modules
that live anywhere — a shared directory in your repository, or Slang files
shipped inside a separately installed package — while the compiler itself
discovers nothing and knows none of them by name.
Validating and testing shaders
Two levels, both without a browser or a GPU farm:
- Statically,
glslangtype-checks the emitted GLSL as part of compiling, so a malformed shader fails the build. - Executably,
render_readbackruns the compiled shader in a headless OpenGL context via moderngl, feeding it an array of input values and handing back what the GPU computed. Because the result is a NumPy array, an ordinarypytestcan diff real GPU output against a NumPy reference implementation — so a shader library can prove its math means what its authors think it means, on every commit.
What you get back
Results come back in memory — nothing this package writes to disk is meant to outlive the call, and it never chooses where anything goes.
| Call | Returns |
|---|---|
compile_stages |
{stage: glsl} — GLSL ES 3.00 text, precision declared, varyings renamed so the stages link |
reflect_uniforms |
{name: UniformLayout} — std140 offset, size, stride, and encoding per uniform |
reflect_attributes |
{name: AttributeLayout} — location, element type, and whether it binds as an integer attribute |
pack_ubo |
bytes — one std140 uniform block, ready to upload |
render_readback |
One (N, out_channels) array per uniform set, in the requested texel format |
You supply the source, an entry-point map, the import search paths, a work
directory, and a resolved Toolchain. Anything written along the way is an
intermediate in that directory and can be deleted the moment the call returns —
docs/design.md explains why the package deliberately owns no
format of its own.
Installation
pip install vcti-shader-compiler
Requires Python 3.12 or 3.13. The Python package is pure orchestration — the actual compilers are external binaries you provision yourself, see below.
Prerequisites: the shader toolchain
Three external executables must be on your machine before anything compiles:
slangc, spirv-cross, and glslang. They are not pip-installable — you
download or build them once and point three environment variables at them:
export SLANG_DIR=... # extracted slang release
export SPIRV_CROSS_DIR=... # SPIRV-Cross source tree you built
export GLSLANG_DIR=... # glslang source tree you built
Then check that all three resolve:
python -c "from vcti.shader.compiler import discover_toolchain; print(discover_toolchain())"
docs/toolchain.md has the full procedure — where to
download each one, the cmake invocations for the two that need building, the
layouts each variable expects, and known-good versions.
Quick Start
import tempfile
from pathlib import Path
from vcti.shader.compiler import find_toolchain, compile_stages
toolchain = find_toolchain() # or discover_toolchain() to raise if missing
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)
(work / "demo.slang").write_text(slang_source)
stages = compile_stages(
toolchain,
work / "demo.slang",
{"vertex": "vertexMain", "fragment": "fragmentMain"},
work,
include_dirs=[...], # Slang dirs the source imports from
)
# stages == {"vertex": "<glsl es>", "fragment": "<glsl es>"}
Dependencies
numpy and moderngl — the latter drives the headless GL context that
render_readback tests shaders in. Compiling and testing are one package, not two:
generating a shader you cannot execute is only half the job.
moderngl's glcontext layer publishes no cp314 wheel, which is why
requires-python is capped below 3.14.
The three toolchain executables are external and pinned — they are provisioned as described under Prerequisites, never as pip dependencies.
Documentation
| If you want to… | Read |
|---|---|
| Install the three external compilers | docs/toolchain.md |
Lower precision to mediump, and what half does |
docs/precision.md |
| Add a WGSL target (not implemented — the plan and what was tried) | docs/wgsl.md |
| Understand the architecture and design decisions | docs/design.md |
| Navigate and modify the source | docs/source-guide.md |
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vcti_shader_compiler-2.0.1.tar.gz.
File metadata
- Download URL: vcti_shader_compiler-2.0.1.tar.gz
- Upload date:
- Size: 46.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75a1e57c621be3aeda1d64dbd010867448b3aec3366046dd66f18616ac8d3859
|
|
| MD5 |
72856e749fc8bfebf3c7d4c7f296d00d
|
|
| BLAKE2b-256 |
ee8d33fcfa934b6cf96884843ad91e7d663bb1ced4669b87a22afeb8b1a03b44
|
Provenance
The following attestation bundles were made for vcti_shader_compiler-2.0.1.tar.gz:
Publisher:
release.yml on vcollab/vcti-python-shader-compiler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vcti_shader_compiler-2.0.1.tar.gz -
Subject digest:
75a1e57c621be3aeda1d64dbd010867448b3aec3366046dd66f18616ac8d3859 - Sigstore transparency entry: 2481961638
- Sigstore integration time:
-
Permalink:
vcollab/vcti-python-shader-compiler@b7b8aed122d674a5c3dc801b3cb5c674dd3f567f -
Branch / Tag:
refs/tags/v2.0.1 - Owner: https://github.com/vcollab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b7b8aed122d674a5c3dc801b3cb5c674dd3f567f -
Trigger Event:
push
-
Statement type:
File details
Details for the file vcti_shader_compiler-2.0.1-py3-none-any.whl.
File metadata
- Download URL: vcti_shader_compiler-2.0.1-py3-none-any.whl
- Upload date:
- Size: 27.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54ef13368a0bc476fb8b9107d8f9afa6ab27632c609482a410a07035469f7892
|
|
| MD5 |
d2e6242efffeba6277b3002590b6c05b
|
|
| BLAKE2b-256 |
538da547bc8b9afa8a3f8ac05d69726e4c180d20f1881d9ccb2d5e3bb41eee19
|
Provenance
The following attestation bundles were made for vcti_shader_compiler-2.0.1-py3-none-any.whl:
Publisher:
release.yml on vcollab/vcti-python-shader-compiler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vcti_shader_compiler-2.0.1-py3-none-any.whl -
Subject digest:
54ef13368a0bc476fb8b9107d8f9afa6ab27632c609482a410a07035469f7892 - Sigstore transparency entry: 2481961656
- Sigstore integration time:
-
Permalink:
vcollab/vcti-python-shader-compiler@b7b8aed122d674a5c3dc801b3cb5c674dd3f567f -
Branch / Tag:
refs/tags/v2.0.1 - Owner: https://github.com/vcollab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b7b8aed122d674a5c3dc801b3cb5c674dd3f567f -
Trigger Event:
push
-
Statement type: