Skip to main content



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, and d3d12 resolve 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 core when absent).
    • Handles interface blocks, layouts/bindings, sampler & image operations, control flow, structs/arrays, discard, builtins (gl_*) and preprocessor directives.
  • Round‑trips verified via converters in crosstl.backend.{DirectX,Metal,GLSL} and registered in translator.source_registry and translator.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:

  1. Lexical Analysis: Tokenization
  2. Syntax Analysis: AST generation
  3. Semantic Analysis: Type checking and scope resolution
  4. IR Generation: Conversion to CrossGL intermediate representation
  5. 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

Metadata

Release files for crosstl 3.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for crosstl 3.3.0
File Size Uploaded
crosstl-3.3.0.tar.gz 3.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for crosstl 3.3.0
File Interpreter ABI Platform
crosstl-3.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 6.6 MB

Release files / crosstl-3.3.0.tar.gz

Download URL crosstl-3.3.0.tar.gz
Size 3.5 MB
Tags Source
SHA-256 checksum
How to use checksums
ee947587c3c18dee8248a4b254fae9d67af525b9633553584ec27f3c8e101df7
BLAKE2b-256 checksum
How to use checksums
986f453551a61995d7868b0f0f0692e2771850d4314096120016a70ab51daada
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.12.14

Release files / crosstl-3.3.0-py3-none-any.whl

Download URL crosstl-3.3.0-py3-none-any.whl
Size 3.1 MB
Tags Python 3
SHA-256 checksum
How to use checksums
c6890d35da3882c27c45b97fc1168c728e22392371e4b22befbe2c86ae5f7940
BLAKE2b-256 checksum
How to use checksums
31bfebe0e98d95bec62d38ad4bb9fcb28085d24f2e552065bf0a91e69abf0200
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

3.3.0 This release

2 release files

3.2.0

2 release files

3.1.0

2 release files

3.0.0

1 release file

2.0.1

2 release files

2.0.0

2 release files

1.0.0

1 release file

0.0.1.3

1 release file

0.0.1.2

1 release file

0.0.1.1

1 release file

0.0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page