Skip to main content

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 whichever graphics API has to run it — GLSL ES 3.00 today, WGSL next. 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 two 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.

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 #define switches. 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. run_probe runs 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 ─→ std140 uniform layouts

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 two of its defects are invisible until far downstream:

  • 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 shared v{location}.
  • Float precision is promoted to highp. SPIRV-Cross defaults ES floats to mediump, which is as little as 10 bits of mantissa — fine for colours, but lossy for any shader that consumes measured or computed data, or large coordinates. The symptom would be quietly wrong pixels rather than an error.

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, glslang type-checks the emitted GLSL as part of compiling, so a malformed shader fails the build.
  • Executably, run_probe runs 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 ordinary pytest can 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, highp, varyings renamed so the stages link
reflect_uniforms {name: UniformLayout} — std140 offset, size, stride, and encoding per uniform
pack_ubo bytes — one std140 uniform block, ready to upload
run_probe One (N, out_channels) float32 array per uniform set

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 run_probe 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
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

vcti_shader_compiler-1.1.0.tar.gz (30.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vcti_shader_compiler-1.1.0-py3-none-any.whl (19.5 kB view details)

Uploaded Python 3

File details

Details for the file vcti_shader_compiler-1.1.0.tar.gz.

File metadata

  • Download URL: vcti_shader_compiler-1.1.0.tar.gz
  • Upload date:
  • Size: 30.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vcti_shader_compiler-1.1.0.tar.gz
Algorithm Hash digest
SHA256 1a2f856ccaaea9783fa097bc9427e11029a9084481eb3e20451be1ca69f527f3
MD5 a0cd288e4181e51a1c9ac04c6ba4eef5
BLAKE2b-256 8a06eb7a5f8b573d2e7428fa1d07eb8d8bc72c7423a17b692f9f8cc9a158b81f

See more details on using hashes here.

Provenance

The following attestation bundles were made for vcti_shader_compiler-1.1.0.tar.gz:

Publisher: release.yml on vcollab/vcti-python-shader-compiler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vcti_shader_compiler-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for vcti_shader_compiler-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cb1bae04b4b36e6fbb08b7daa11f990457cf341bb333c88f4337f674efa06c2e
MD5 7f0100c19f09f1c9e9fab5c2756a3a4c
BLAKE2b-256 58fdf8d094d0ea35f5d97be2bf4593ab36f6e8c2f41b2a9fd328b604dd87ccd9

See more details on using hashes here.

Provenance

The following attestation bundles were made for vcti_shader_compiler-1.1.0-py3-none-any.whl:

Publisher: release.yml on vcollab/vcti-python-shader-compiler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

3.0.0

2 files

2.0.1

2 files

2.0.0

2 files

This release

1.1.0 This release

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page