
I am building wintty, a Windows-native terminal based on Ghostty. Before it can draw a single frame it has to compile its shaders, and the industry answer to "compile shaders" is two C++ projects: glslang to turn GLSL into SPIR-V, and SPIRV-Cross to turn SPIR-V into whatever your GPU actually speaks. On Windows that meant shipping both, building them with CMake inside a Zig build, and either linking the C++ or spawning a subprocess per shader. A process spawn costs about 150 ms on Windows. Ten shaders, nearly two seconds of startup, before the first pixel.
So I did the reasonable thing, which is to say the thing that seemed small at the time: I wrote a little GLSL preprocessor in Zig to start eating that stack.
It did not stay little. The artisan's tools are never finished, only current; this one is on its Mk 2400 and counting.
If the Mk 2400 bit landed without context: I wrote about that whole mindset in The artisan's tools. Worth a read if you want the longer version of what I mean when I say the tools are never finished, only current.
The slope, slid down
The preprocessor became a frontend. The frontend learned to emit SPIR-V. Then SPIR-V needed to become HLSL, because this is Windows, and once you have one backend the others start asking. Four months and 2400+ commits later it is zioshade: GLSL to SPIR-V to HLSL, MSL, GLSL, and WGSL, in one Zig module. 98k lines of Zig in the library, 53k lines of tests, zero lines of C++. The only C++ file in the repository is the Windows test harness that renders the output on D3D12 to prove it is correct.
There were stretches where I was sure I would not pull it off. SPIR-V semantics are a maze, HLSL has opinions about everything, and a green validator pass can still be lying to you. I kept researching, kept trying, kept adding oracles because I did not trust my own eyes. Then something shifted: the failures stopped feeling like verdicts and started feeling like a map. Each rejection, each pixel diff, each "naga says no" was telling me where the real boundary was. That kept me going long enough to merge it into wintty, then keep testing, keep tightening, keep widening scope a little at a time, until today.
wintty has been running on it in production since July. The pull request that deleted glslang and SPIRV-Cross from the build is the most satisfying diff I have ever merged.
For wintty the point was never a benchmark slide. A terminal that makes you wait two seconds to draw a frame feels broken before you type a character. Here is what swapping the stack actually bought:
| Metric | Before | After |
|---|---|---|
| Toolchain | glslang + SPIRV-Cross, CMake, C++ link or subprocess | one Zig dependency |
| 10-shader startup | ~1.8 s of process spawning | ~10 ms, in-process |
| Subprocess pipeline | 150-265x slower than zioshade | 732 µs to 1.2 ms per shader |
| Library vs library | SPIRV-Cross C API, in-process | 1.4-1.6x faster on the median shader |
The honest caveat on the last row: most of the 150-265x is spawn overhead, not algorithmic genius. The library-vs-library number, where both sides run in-process, is the one that says anything about the compiler itself.
The part that was actually hard
Emitting plausible-looking HLSL is easy. I could show you output that compiles clean in every validator and still computes the wrong thing. Compile-clean is not render-correct, and a shader compiler that is subtly wrong is worse than one that crashes: your terminal renders, it is just quietly wrong.
So the real work became verification. The rule I settled on: the output is judged by the competitors' own tools, never by me.
- Every SPIR-V byte goes through the Khronos validator, on a 2100+ fixture gate, on every commit.
- The MSL backend is render-proven on a real Metal GPU: zioshade's output and an independent glslang+SPIRV-Cross reference both render, and the pixels must match. 1300+ shaders, zero divergences, plus a second independent oracle (naga) so a mistake the two of us share cannot hide.
- The HLSL backend now has the same treatment on the D3D12 side: DXC compiles both outputs to DXIL, a WARP harness renders them, pixels diff. Full corpus: 1374 render-matches, 5 diffs, and all five are proven benign floating-point contraction (recompile both sides with strict IEEE and they render pixel-identical).
- The WGSL backend is checked against the WebGPU CTS itself. This one is my favorite: the harness extracts the CTS's own per-case shaders by driving its framework with a faked GPU device, then round-trips them through zioshade and validates. 1613 real CTS-authored shaders, 1597 round-trip valid, zero invalid, zero crashes.
- A structured fuzzer ran a million iterations clean.
And the principle underneath all of it: when zioshade cannot translate something faithfully, it refuses, by name. A rejected shader is a bug report. A silently miscompiled shader is a trap. The refusals are counted and published, because "we know exactly what we do not support" is a feature.
What it is not
It is not a Khronos drop-in. If you need the full GLSL ES surface, complete descriptor reflection, or SPIRV-Cross's twenty years of corner cases, use SPIRV-Cross; it is excellent and it is why I could verify against it. zioshade is deliberately narrow: the shading language real projects actually have (GLSL 330-460), to the four targets they actually need, with an in-process Zig module and no toolchain behind it.
It is also, so far, one maintainer. I think the answer to "why trust a one-person compiler" has to be the verification stack, not the author. Every claim above has a one-command reproduction in the repository. That was the design constraint: make checking the work cheaper than trusting the author.
Was this AI-built?
Heavily AI-assisted, and that is why the verification exists rather than a reason to discount the result. The workflow puts reference implementations in the judging seat: adversarial review on every change, then spirv-val, DXC, naga, tint, glslang, and two real GPUs rendering pixel diffs. The tools do the writing; the reference implementations do the judging. A one-maintainer compiler with this much external judging is, I would argue, more auditable than most, not less.
Try it
1zig build cli2zig-out/bin/zioshade hlsl examples/shader.frag -o shader.hlsl
Or as a dependency: zig fetch the v0.7.0 tarball, one import, done. MIT or
Apache-2.0, your pick, C ABI included if you are not in Zig.
The homestead rule applies here too: the toolchain you need does not exist, so you build it, and you prove it with receipts before you ask anyone to trust it.
One more thing. Ghostty itself, upstream, still carries the same two C++ dependencies for its custom-shader feature today. I checked this morning. Same stack, same shape of problem. If that is interesting to anyone upstream: the tooling to prove the swap is safe now exists, and I am easy to find.