CrossTL - Universal Shader Translator
CrossTL is a shader and compute program translator built around CrossGL — an intermediate representation (IR) language that bridges graphics APIs, GPU-compute platforms, and systems-language targets.
Supported Translation Targets
CrossTL provides translation from CrossGL to:
- Metal - Apple's graphics and compute API
- DirectX (HLSL) - Microsoft's graphics API
- OpenGL (GLSL) - Cross-platform graphics
- WebGL (GLSL ES) - Browser graphics through WebGL 2.0 compatible GLSL ES
- WebGPU (WGSL) - Browser and native WebGPU shader output
- Vulkan (SPIRV) - High-performance graphics and compute
- CUDA - NVIDIA parallel computing
- HIP - AMD GPU computing
- Rust - GPU-oriented Rust shader code
- Mojo - Mojo shader/compute modules
- Slang - Real-time shading language
- CrossGL (.cgl) - The IR/interchange format itself
Native source import is available for the bidirectional backends listed in the
support matrix. WebGL and WGSL are currently target-only outputs; .webgl.glsl,
.wgsl, and .wesl inputs are rejected until dedicated source frontends
land.
Backend Readiness: DirectX / Metal / OpenGL
We maintain first-class, bidirectional support for the three cornerstone graphics APIs. Each backend is implemented as both a source (parse/import) and codegen (export) target, so you can round‑trip between native shaders and CrossGL without lossy hops.
- DirectX / HLSL
- Pipeline coverage: vertex, fragment/pixel, compute, geometry, hull/domain (tessellation), mesh/task, full ray‑tracing stages.
- Target profile aliases:
dx11,dx12,d3d11, andd3d12resolve to the HLSL emitter for Direct3D deployment planning; final DXBC/DXIL packaging remains a toolchain step. - Resource model: cbuffers, register/space bindings, UAV/RW textures & buffers, structured buffers, Interlocked atomics, wave ops, texture/buffer dimension queries.
- Semantics map to
SV_*and user semantics, preserved through CrossGL attributes.
- Metal
- Stages: vertex, fragment, compute, mesh/object, and ray‑tracing qualifiers.
- Resource/binding support: argument buffers,
[[buffer]],[[texture]],[[sampler]], function constants, packed/simd types, indirect command buffers, payload/hit attributes. - Texture methods (sample/load/store/gather/compare) and threadgroup builtins are translated to CrossGL intrinsics.
- OpenGL / GLSL
- Stages: vertex, fragment, compute with version inference (defaults to
#version 450 corewhen absent). - Handles interface blocks, layouts/bindings, sampler & image operations, control flow, structs/arrays, discard, builtins (
gl_*) and preprocessor directives.
- Stages: vertex, fragment, compute with version inference (defaults to
- Round‑trips verified via converters in
crosstl.backend.{DirectX,Metal,GLSL}and registered intranslator.source_registryandtranslator.codegen.registry.
How we validate backend parity
- Targeted unit suites exercise the full feature surface for these three backends (shader stages, bindings, intrinsics, control‑flow, resources, and preprocessor handling).
- Quick check: run
pytest tests/test_backend/test_directx tests/test_backend/test_metal tests/test_backend/test_GLSL -q(currently 321 passing tests). - End‑to‑end translation: translate native HLSL/Metal/GLSL → CrossGL → HLSL/Metal/GLSL/Vulkan to ensure attributes, layouts, and semantics survive round‑trips.
Translation Architecture
CrossTL uses a multi-stage translation pipeline:
- Lexical Analysis: Tokenization
- Syntax Analysis: AST generation
- Semantic Analysis: Type checking and scope resolution
- IR Generation: Conversion to CrossGL intermediate representation
- Target Generation: Backend-specific code generation
CrossGL Programming Language Examples
PBR Shader Example
The README demo uses the same current CrossGL source file as the showcase
notebook: demos/readme/showcase_pbr.cgl and
demos/readme/demo.ipynb. The shader demonstrates a
real PBR-style material path: tangent-space normal mapping, GGX normal
distribution, Smith geometry, Schlick Fresnel, multiple point lights, texture
sampling, tone mapping, and gamma correction.
It uses the current struct-return stage style so backends such as Metal can
emit proper stage input/output structs and vertex/fragment entry points.
shader ShowcasePBRShader {
const float PI = 3.14159265359;
const float EPSILON = 0.0001;
const int MAX_LIGHTS = 4;
struct VertexInput {
vec3 position;
vec3 normal;
vec3 tangent;
vec2 texCoord;
}
struct VertexOutput {
vec3 worldPosition;
vec3 worldNormal;
vec3 worldTangent;
vec3 worldBitangent;
vec2 uv;
vec4 position;
}
struct FragmentInput {
vec3 worldPosition;
vec3 worldNormal;
vec3 worldTangent;
vec3 worldBitangent;
vec2 uv;
}
struct FragmentOutput {
vec4 color;
}
float distributionGGX(vec3 normal, vec3 halfVector, float roughness) {
float alpha = roughness * roughness;
float alpha2 = alpha * alpha;
float ndoth = max(dot(normal, halfVector), 0.0);
float denom = ndoth * ndoth * (alpha2 - 1.0) + 1.0;
return alpha2 / max(PI * denom * denom, EPSILON);
}
float geometrySchlickGGX(float ndotv, float roughness) {
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
return ndotv / max(ndotv * (1.0 - k) + k, EPSILON);
}
float geometrySmith(vec3 normal, vec3 viewDir, vec3 lightDir, float roughness) {
float ndotv = max(dot(normal, viewDir), 0.0);
float ndotl = max(dot(normal, lightDir), 0.0);
return geometrySchlickGGX(ndotv, roughness) * geometrySchlickGGX(ndotl, roughness);
}
vec3 fresnelSchlick(float cosTheta, vec3 baseReflectance) {
return baseReflectance + (vec3(1.0) - baseReflectance) * pow(max(1.0 - cosTheta, 0.0), 5.0);
}
vertex {
uniform mat4 modelMatrix;
uniform mat4 viewProjectionMatrix;
uniform mat3 normalMatrix;
VertexOutput main(VertexInput input) {
VertexOutput output;
vec4 worldPosition = modelMatrix * vec4(input.position, 1.0);
output.worldPosition = worldPosition.xyz;
output.worldNormal = normalize(normalMatrix * input.normal);
output.worldTangent = normalize(normalMatrix * input.tangent);
output.worldBitangent = normalize(cross(output.worldNormal, output.worldTangent));
output.uv = input.texCoord;
output.position = viewProjectionMatrix * worldPosition;
return output;
}
}
fragment {
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicRoughnessMap;
uniform vec3 cameraPosition;
uniform vec3 lightPositions[MAX_LIGHTS];
uniform vec3 lightColors[MAX_LIGHTS];
uniform int activeLightCount;
uniform vec3 baseColor;
uniform float metallicFactor;
uniform float roughnessFactor;
vec3 decodeNormal(vec3 encodedNormal, mat3 tangentFrame) {
vec3 tangentNormal = encodedNormal * 2.0 - 1.0;
return normalize(tangentFrame * tangentNormal);
}
FragmentOutput main(FragmentInput input) {
FragmentOutput output;
mat3 tangentFrame = mat3(
normalize(input.worldTangent),
normalize(input.worldBitangent),
normalize(input.worldNormal)
);
vec3 normal = decodeNormal(texture(normalMap, input.uv).rgb, tangentFrame);
vec3 viewDir = normalize(cameraPosition - input.worldPosition);
vec3 albedo = texture(albedoMap, input.uv).rgb * baseColor;
vec3 materialSample = texture(metallicRoughnessMap, input.uv).rgb;
float metallic = clamp(materialSample.b * metallicFactor, 0.0, 1.0);
float roughness = clamp(materialSample.g * roughnessFactor, 0.04, 1.0);
vec3 baseReflectance = mix(vec3(0.04), albedo, metallic);
vec3 radianceSum = vec3(0.0);
for (int i = 0; i < MAX_LIGHTS; i++) {
if (i >= activeLightCount) {
break;
}
vec3 lightVector = lightPositions[i] - input.worldPosition;
float distanceSq = max(dot(lightVector, lightVector), EPSILON);
vec3 lightDir = normalize(lightVector);
vec3 halfVector = normalize(viewDir + lightDir);
float attenuation = 1.0 / distanceSq;
vec3 radiance = lightColors[i] * attenuation;
float normalDistribution = distributionGGX(normal, halfVector, roughness);
float geometry = geometrySmith(normal, viewDir, lightDir, roughness);
vec3 fresnel = fresnelSchlick(max(dot(halfVector, viewDir), 0.0), baseReflectance);
vec3 diffuseShare = (vec3(1.0) - fresnel) * (1.0 - metallic);
vec3 numerator = normalDistribution * geometry * fresnel;
float denominator = max(4.0 * max(dot(normal, viewDir), 0.0) * max(dot(normal, lightDir), 0.0), EPSILON);
vec3 specular = numerator / denominator;
float ndotl = max(dot(normal, lightDir), 0.0);
radianceSum += (diffuseShare * albedo / PI + specular) * radiance * ndotl;
}
vec3 ambient = albedo * 0.03;
vec3 color = ambient + radianceSum;
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0 / 2.2));
output.color = vec4(color, 1.0);
return output;
}
}
}
Run the showcase notebook or translate the source directly:
python -m crosstl translate demos/readme/showcase_pbr.cgl --backend metal --output -
python -m crosstl translate demos/readme/showcase_pbr.cgl --backend directx --output -
python -m crosstl translate demos/readme/showcase_pbr.cgl --backend opengl --output -
Backend Compatibility
All backends support the core CrossGL language (structs, arrays, functions, control flow, texture sampling, shader stages). Some advanced language features have partial backend coverage:
| Feature | Not yet supported in |
|---|---|
| Generic functions | SPIR-V, CUDA, HIP, Mojo, Slang (pending monomorphization) |
| Geometry stage | Metal, CUDA, HIP, Mojo, Rust (diagnostic fallback) |
| Tessellation stage | OpenGL, Metal, CUDA, HIP, Mojo, Rust (diagnostic fallback) |
| Mesh/Task stage | CUDA, HIP, Mojo, Rust, Slang (diagnostic fallback) |
Translation calls targeting unsupported feature/backend combinations raise a clear error with a reason string.
Getting Started
Install CrossTL:
pip install crosstl
Basic Usage
1. Create a CrossGL shader
shader SimpleShader {
struct VertexInput {
vec3 position;
vec2 texCoord;
}
struct VertexOutput {
vec2 uv;
vec4 position;
}
struct FragmentInput {
vec2 uv;
}
struct FragmentOutput {
vec4 color;
}
vertex {
VertexOutput main(VertexInput input) {
VertexOutput output;
output.uv = input.texCoord;
output.position = vec4(input.position, 1.0);
return output;
}
}
fragment {
FragmentOutput main(FragmentInput input) {
FragmentOutput output;
output.color = vec4(input.uv, 0.5, 1.0);
return output;
}
}
}
2. Translate to a target
import crosstl
# Translate to any supported backend
metal_code = crosstl.translate('shader.cgl', backend='metal', save_shader='shader.metal')
hlsl_code = crosstl.translate('shader.cgl', backend='directx', save_shader='shader.hlsl')
dx12_hlsl = crosstl.translate('shader.cgl', backend='dx12', save_shader='shader.hlsl')
glsl_code = crosstl.translate('shader.cgl', backend='opengl', save_shader='shader.glsl')
Project audit workflow
For repositories with many shader or GPU source files, start with a scan-only report before writing translated artifacts:
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/portability-report.json
python -m crosstl validate-project \
crosstl-out/portability-report.json \
--format text
python -m crosstl inspect-report \
crosstl-out/portability-report.json \
--format text
Project reports cover shader and kernel source translation, diagnostics,
artifact provenance, optional toolchain checks, and manual migration actions
for host/runtime integration. See the project porting guide.
The repository also includes pinned open-source porting demos with checked
artifacts and platform validator coverage in
demos/open-source-porting.
Reverse Translation - Import Existing Code
# Import existing shaders into CrossGL
conversions = [
('existing_shader.hlsl', 'unified.cgl'), # DirectX to CrossGL
('gpu_kernel.cu', 'unified.cgl'), # CUDA to CrossGL
('graphics.metal', 'unified.cgl'), # Metal to CrossGL
]
for source, target in conversions:
unified_code = crosstl.translate(source, backend='cgl', save_shader=target)
print(f"Unified {source} into CrossGL: {target}")
Deploying to All Backends
import crosstl
from pathlib import Path
program = 'universal_shader.cgl'
deployment_targets = {
'metal': '.metal',
'directx': '.hlsl',
'dx12': '.hlsl',
'opengl': '.glsl',
'vulkan': '.spvasm',
'rust': '.rs',
'mojo': '.mojo',
'cuda': '.cu',
'hip': '.hip',
'slang': '.slang',
}
stem = Path(program).stem
for backend, extension in deployment_targets.items():
output_file = f'deployments/{stem}_{backend}{extension}'
try:
crosstl.translate(program, backend=backend, save_shader=output_file)
print(f"{backend}: {output_file}")
except Exception as e:
print(f"{backend}: {str(e)}")
For comprehensive language documentation, visit our Language Reference.
Contributing
CrossGL is a community-driven project. Find out more in our Contributing Guide.
Community
License
CrossTL is open-source and licensed under the Apache License 2.0.
The CrossGL Team
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file crosstl-3.1.0.tar.gz.
File metadata
- Download URL: crosstl-3.1.0.tar.gz
- Upload date:
- Size: 3.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc6c330c7fcb50fb385092c316b73653f5bb4439a9bef8d9a1dd14c31fb73480
|
|
| MD5 |
d66ae8a7bfb03e790a0b569eeb3ddc15
|
|
| BLAKE2b-256 |
b08cd9ebe2194a55be1a5b41039da88eeaf1a7b1954173f4fddc3180dd3c9009
|
File details
Details for the file crosstl-3.1.0-py3-none-any.whl.
File metadata
- Download URL: crosstl-3.1.0-py3-none-any.whl
- Upload date:
- Size: 2.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc17b50d4996cb8002d9f3699d61171fdf6450adb874b5501ccbb783e39aa315
|
|
| MD5 |
6e647583e4dae838f6def5a66a0c3666
|
|
| BLAKE2b-256 |
d9dfd2da1aa4876bc831b6be64d07abaa01cdc521abfeed37a989fd7ac750fd7
|