Profile
Back to NewsBack
GitHub Trending 5 min
Reader Mode
CrossGL/crosstl: 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

CrossGL/crosstl: 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

17 hours ago

CrossTL

Tests</a> PyPI</a> License</a> DOI</a>

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.

!CrossTL coverage per language and direction

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

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

A measured direction has a row in COVERAGE.md. An available direction is implemented and unit-tested but not yet measured. 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 in all the measured 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// 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//. 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.

Chat with me