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.
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.
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.
- 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.
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
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 -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.
Install CrossTL:
pip install crosstlshader 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;
}
}
}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')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 textProject 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.
# 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}")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.
CrossGL is a community-driven project. Find out more in our Contributing Guide.
CrossTL is open-source and licensed under the Apache License 2.0.
The CrossGL Team
