Translate GPU shaders and compute kernels between GLSL, HLSL, Metal, SPIR-V and Slang, from CUDA and HIP, and to WGSL, through the CrossGL IR
CrossTL
CrossTL translates GPU shaders and compute kernels between languages. Every translation goes through CrossGL, a small intermediate language: a frontend imports GLSL, HLSL, Metal or another source language into CrossGL, and a code generator writes CrossGL out as GLSL, HLSL, Metal, SPIR-V, WGSL or another target. You can also write CrossGL directly and generate every target from one source file.
CI measures this chart on every push to main and nightly. Every feature in a pinned upstream feature list gets a
generated probe, CrossTL translates it, and the language's reference compiler or validator checks the result.
Compute probes also run, on the GPU or on Mesa's software drivers, where the translation must compute what the
native probe computes: Metal, GLSL and SPIR-V directly, and HLSL, Slang and WGSL as the SPIR-V their reference
compilers produce. The score is supported / (supported + refused + wrong). COVERAGE.md
lists every gap behind each bar.
Install
pip install crosstl
CrossTL needs Python 3.11 or later. Validating the generated code needs the target's own compiler or validator
(glslang, DXC, spirv-tools, naga, the Metal compiler, nvcc, hipcc, slangc, rust-gpu or the Mojo compiler), which
CrossTL does not install. crosstl doctor lists the tools it looks for and which of them are on PATH.
Quickstart
Save this compute kernel as saxpy.cgl:
shader Saxpy {
compute {
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
buffer float* x;
buffer float* y;
uniform float a;
void main() {
uint i = gl_GlobalInvocationID.x;
y[i] = a * x[i] + y[i];
}
}
}
Then translate it from Python, where the source language comes from the file extension:
import crosstl
metal = crosstl.translate("saxpy.cgl", backend="metal")
crosstl.translate("saxpy.cgl", backend="wgsl", save_shader="saxpy.wgsl")
# A native shader is imported into CrossGL first, then generated for the target.
glsl = crosstl.translate("blur.hlsl", backend="opengl")
crossgl = crosstl.translate("blur.hlsl", backend="cgl")
or from the command line:
crosstl translate saxpy.cgl --backend directx --output saxpy.hlsl
crosstl translate saxpy.cgl --backend vulkan --output -
python -m crosstl is the same command. The examples/ directory has larger CrossGL programs.
Languages
| Language | backend |
Output | Input |
|---|---|---|---|
| GLSL | opengl |
measured | measured |
| HLSL | directx |
measured | measured |
| Metal | metal |
measured | measured |
| SPIR-V assembly | vulkan |
measured | measured |
| WGSL | wgsl |
measured | not offered |
| GLSL ES 3.00 (WebGL 2) | webgl |
measured | not offered |
| CUDA | cuda |
measured | measured |
| HIP | hip |
measured | measured |
| Slang | slang |
measured | measured |
| Rust (rust-gpu) | rust |
measured | measured |
| Mojo | mojo |
measured | measured |
| OpenCL C | not offered | measured |
A measured direction has a row in COVERAGE.md.
CrossTL has no OpenCL C generator, so the OpenCL C input row checks the HLSL that CrossTL writes from each OpenCL C
probe. The language names glsl, hlsl, msl and spirv work as backend names too, and cgl writes the CrossGL a
native source imports to. CrossGL has its own coverage row, CrossGL IR, which measures how many of the features of
all the measured output languages CrossGL can express.
CrossTL is meant to fail closed: a construct it cannot translate raises a diagnostic that names the construct and the target, instead of emitting a placeholder. Output that the reference tool rejects counts as wrong in the coverage report, and the Coverage workflow files it as a bug.
How it works
.hlsl .metal .glsl .cu ... .glsl .hlsl .metal .spvasm .wgsl ...
native source --> frontend --> CrossGL source --> CrossGL parser --> code generator --> target source
(parse and (AST and types) (one per target)
import)
Each source language has a frontend in crosstl/backend/<Language>/ that parses it and writes CrossGL. The CrossGL
parser in crosstl/translator/ builds the typed AST that every code generator in crosstl/translator/codegen/
reads, so a feature added to CrossGL is available to every target that can express it.
Porting a repository
For a project with many shader or kernel sources, scan it before writing anything, then translate it and check the report:
python -m crosstl scan path/to/repo --target metal --output scan-report.json
python -m crosstl translate-project path/to/repo --target metal --output-dir crosstl-out \
--report crosstl-out/report.json
python -m crosstl validate-project crosstl-out/report.json --format text
python -m crosstl inspect-report crosstl-out/report.json --format text
A relative --output-dir is resolved against the project, so the translated files land in
path/to/repo/crosstl-out/<target>/. The report records each translated file, its diagnostics and provenance, the
reference toolchain checks that ran, and the host-side changes that remain manual. The project porting guide covers
the options, and demos/ has pinned open-source ports with checked artifacts.
Documentation
The user documentation is at docs.crossgl.net.
The Sphinx sources, including the architecture notes and decision records, are in docs/source.
Contributing
Read CONTRIBUTING.md for setup, the test tiers and the pull request flow. Issues labelled good first issue are a good place to start, and questions belong in Discussions or on Discord. Report security problems as described in SECURITY.md.
Citation
If you use CrossTL in your work, cite it with the metadata in CITATION.cff; GitHub's "Cite this repository" button produces BibTeX and APA from it. Each release is archived on Zenodo under 10.5281/zenodo.15826974.
License
CrossTL is licensed under the Apache License 2.0.