Why my terminal needed its own shader compiler - Alessandro De Blasis
Skip to content
Why my terminal needed its own shader compiler<br>To draw its first frame on Windows, my terminal had to ship two C++ projects and spawn a subprocess per shader. Four months and 2,400 commits later, that…<br>21 August 2026<br>techziggraphics
On this pageThe slope, slid down<br>The part that was actually hard<br>What it is not<br>Was this AI-built?<br>Try it
I am building wintty, a Windows-native terminal based on<br>Ghostty. Before it can draw a single frame it has to compile its shaders, and<br>the industry answer to "compile shaders" is two C++ projects: glslang to turn<br>GLSL into SPIR-V, and SPIRV-Cross to turn SPIR-V into whatever your GPU<br>actually speaks. On Windows that meant shipping both, building them with CMake<br>inside a Zig build, and either linking the C++ or spawning a subprocess per<br>shader. A process spawn costs about 150 ms on Windows. Ten shaders, nearly two<br>seconds of startup, before the first pixel.
So I did the reasonable thing, which is to say the thing that seemed small at<br>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;<br>this one is on its Mk 2,400 and counting.
The slope, slid down
The preprocessor became a frontend. The frontend learned to emit SPIR-V.<br>Then SPIR-V needed to become HLSL, because this is Windows, and once you have<br>one backend the others start asking. Four months and 2,400+ commits later it<br>is zioshade: GLSL to SPIR-V to HLSL,<br>MSL, GLSL, and WGSL, in one Zig module. 98k lines of Zig in the library, 53k<br>lines of tests, zero lines of C++. The only C++ file in the repository is the<br>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<br>are a maze, HLSL has opinions about everything, and a green validator pass can<br>still be lying to you. I kept researching, kept trying, kept adding oracles<br>because I did not trust my own eyes. Then something shifted: the failures<br>stopped feeling like verdicts and started feeling like a map. Each rejection,<br>each pixel diff, each "naga says no" was telling me where the real boundary<br>was. That kept me going long enough to merge it into wintty, then keep testing,<br>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<br>deleted glslang and SPIRV-Cross from the<br>build is the most satisfying diff<br>I have ever merged.
For wintty the point was never a benchmark slide. A terminal that makes you<br>wait two seconds to draw a frame feels broken before you type a character.<br>Here is what swapping the stack actually bought:
| | before | after |<br>|---|---|---|<br>| Toolchain | glslang + SPIRV-Cross, CMake, C++ link or subprocess | one Zig dependency |<br>| 10-shader startup | ~1.8 s of process spawning | ~10 ms, in-process |<br>| Subprocess pipeline | 150-265x slower than zioshade | 732 µs to 1.2 ms per shader |<br>| 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,<br>not algorithmic genius. The library-vs-library number, where both sides run<br>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<br>clean in every validator and still computes the wrong thing. Compile-clean is<br>not render-correct, and a shader compiler that is subtly wrong is worse than<br>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<br>judged by the competitors' own tools, never by me.
Every SPIR-V byte goes through the Khronos validator, on a 2,100+ fixture<br>gate, on every commit.
The MSL backend is render-proven on a real Metal GPU: zioshade's output and<br>an independent glslang+SPIRV-Cross reference both render, and the pixels<br>must match. 1,300+ shaders, zero divergences, plus a second independent<br>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<br>both outputs to DXIL, a WARP harness renders them, pixels diff. Full<br>corpus: 1,374 render-matches, 5 diffs, and all five are proven benign<br>floating-point contraction (recompile both sides with strict IEEE and they<br>render pixel-identical).
The WGSL backend is checked against the WebGPU CTS itself. This one is my<br>favorite: the harness extracts the CTS's own per-case shaders by driving<br>its framework with a faked GPU device, then round-trips them through<br>zioshade and validates. 1,613 real CTS-authored shaders, 1,597 round-trip<br>valid, zero invalid, zero crashes.
A structured fuzzer ran a million iterations clean.
And the principle underneath all of it: when zioshade cannot translate<br>something...