CrossTL: Hub-Based Translation of GPU Programs and the Preservation of Source Rules
Abstract
GPU programs are written in vendor languages that disagree about integer overflow, division by zero, NaN handling, floating-point contraction, data layout and dispatch, so a translated program can compile and still compute something else. We describe CrossTL, an open-source translator that routes ten GPU source languages to eleven targets through CrossGL, a textual hub language, with repository-scale porting, contracts that loaders check, and native parity tests. We define faithful translation as refinement up to rejection under a compile configuration, and test whether a hub that records each source rule and lowers it through one per-target table keeps rules that a syntax-only hub loses. On an internally pre-registered suite of 66 hazard kernels in six languages, run on an Apple GPU through Metal, MoltenVK and WebGPU and on CPU implementations of Vulkan, OpenGL and an emulated Direct3D 12, SPIRV-Cross, naga, Tint, Slang and CrossTL each lose some source rule, and outcomes change with Metal's math mode and between two Vulkan implementations. A rule-carrying prototype of the hub removes 34 of 39 divergences on the rules it carries, against 14 for local fixes to four SPIR-V defects, and refuses where WGSL cannot express a rule. The registered hypothesis that it keeps every rule fails: MoltenVK, a non-conformant Vulkan layer, drops a rule that correct SPIR-V carries natively. On 2,585 programs written by third parties, 31% of CrossTL's translations are usable, established translators produce valid output significantly more often in 31 of 39 comparisons, and carrying the rules changes no target's accepted share by more than 0.2 points. Recording each operation's source rule suffices for the rules tested but is not necessary: one strongest rule would keep them too, at about nine times as many rewrites and 58 more refusals on the corpus.
Figures & tables
| Route | Metal, safe | Metal, default | WGSL, wgpu | lavapipe | MoltenVK | D3D12 | llvmpipe |
| CrossTL | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ |
| CrossTL-R | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| glslang SPIRV-Cross | ✗ | ✗ | ✗ | ✓ | |||
| glslang (SPIR-V) | ✓ | ✗ | |||||
| naga (GLSL) | ✓ | ✓ | ✓ | ✓ | |||
| glslang naga | ✓ | ✓ | ✓ | ✓ |
| GLSL | HLSL (DXBC / DXC) | MSL | WGSL | SPIR-V (Vulkan) | CUDA / HIP | OpenCL C | |
| Signed overflow ( ) | wrap | wrap / poison a | UB | wrap | wrap b | UB | UB |
| Shift count width | uval | mask / mask | mask | mask | poison | UB | mask |
| Unsigned | n/s | def ( 0xFFFFFFFF ) / UB c | uval | def ( ) | UB | UB | uval |
| Signed % with negative operand | uval | same-sign only / trunc. | UB | trunc. | poison d | trunc. | trunc. |
| int vs uint compare | unsigned | unsigned | unsigned | error | by opcode | unsigned | unsigned |
| Float int: NaN; out of range | n/s ; n/s | 0, sat / undef e | 0; UB | uval ; sat to | UB | UB | impl ( _sat : 0, sat ) |
| Rule | Defined by | Metal | SPIR-V | HLSL | GLSL | WGSL | CUDA | HIP |
| wrapping | GLSL, HLSL, SPIR-V | P | N | P | N | N | P | P |
| shift, count masked | HLSL, MSL, OpenCL | N | P | N | P | N | P ‡ | P ‡ |
| , 0xFFFFFFFF | HLSL | P | P | P | P | P | P | P |
| truncated % | CUDA, HIP, OpenCL | N ‡ | P | P | P | N | N ‡ | N ‡ |
| 8/16-bit store truncates | MSL, CUDA, HIP, OpenCL | N | P | P | P | P | N | N |
| float int saturates, NaN 0 | HLSL, OpenCL ( convert_int_sat ) | P+C | P ‡ | P ‡ | P+A | Ref | P | P |
| Route | Cases | Pres. | Div. | Hang | Inv. | Ref. | Comp. | N/r |
| CrossTL | 56 | 184 | 87 | 1 | 105 | 9 | 57 | 4 |
| CrossTL-R | 56 | 220 | 55 | 1 | 94 | 16 | 57 | 4 |
| SPIRV-Cross | 15 | 26 | 12 | 0 | 0 | 0 | 0 | 1 |
| naga (GLSL) | 6 | 15 | 0 | 0 | 0 | 3 | 0 | 0 |
| naga (SPIR-V) | 15 | 31 | 8 | 0 | 0 | 0 | 0 | 0 |
| Tint | 15 | 30 | 8 | 0 | 1 | 0 | 0 | 0 |
| Preserved: basis of the match | Not preserved: mechanism | ||||||||
| Route | Target | Constr. | Config. | Impl. | Supported | Fallb. | Wrong | Dropped | Stack |
| CrossTL | 54 | 47 | 11 | 52 | 62 % | 3 | 31 | 27 | 6 |
| CrossTL-R | 39 | 98 | 5 | 46 | 73 % | 0 | 22 | 19 | 6 |
| SPIRV-Cross | 7 | 15 | 0 | 4 | 85 % | 0 | 2 | 10 | 0 |
| naga (GLSL) | 6 | 15 | 1 | 1 | 91 % | 0 | 0 | 0 | 0 |
| naga (SPIR-V) | 6 | 15 | 0 | 2 | 91 % | 0 | 2 | 6 | 0 |
| Arm | Divergences | Source-dependent | Refusals | Invalid |
| CrossTL | 37 | 2 of 26 | 0 | 53 |
| SPIR-V emitter fixes | 23 | 0 of 26 | 0 | 53 |
| CrossTL-R | 5 | 0 of 26 | 7 | 42 |
| CrossTL-R and emitter fixes | 7 | 0 of 26 | 7 | 42 |
Appendix figures & tables20 assets
Supplementary material from the paper’s appendix.
Appendix
| Arm | Divergences | Source-dependent | Refusals | Invalid |
| CrossTL | 51 | 5 of 30 | 0 | 63 |
| SPIR-V emitter fixes | 37 | 3 of 30 | 0 | 63 |
| CrossTL-R | 9 | 1 of 33 | 8 | 52 |
| CrossTL-R and emitter fixes | 11 | 1 of 33 | 8 | 52 |
| Operator | Metal | SPIR-V | HLSL | GLSL | ESSL | WGSL | CUDA | HIP |
| wrap | P | N | P | N | N | N | P | P |
| wrap | P | N | P | N | N | N | P | P |
| wrap | P | N | P | N | N | N | P | P |
| << (u) | N | P | N | P | P | N | P | P |
| >> (u) | N | P | N | P | P | N | P | P |
| << (i) | N | P | N | P | P | N | P | P |
| GLSL | HLSL (DXBC / DXC) | MSL | WGSL | SPIR-V (Vulkan) | CUDA / HIP | OpenCL C | |
| Floating-point environment | |||||||
| Default mode | contraction allowed | fast / fast | fast (API: fast) a | reassoc., fusion | contraction allowed; NaN/Inf assumed absent | FMA on | FP_CONTRACT on |
| Opt-out | precise | precise | #pragma METAL fp , -fno-fast-math | none | NoContraction , float controls | __fmul_rn , --fmad=false | #pragma |
| isnan , NaN handling | may be false b | defined / NaN may be assumed absent | undefined in fast mode | NaN may be assumed absent | assumed absent without SignedZeroInfNanPreserve | defined | defined |
| Rounding of | n/s ( ULP; 2.5 ULP) | RNE for ( may be ) | correctly rounded (safe); RTE or RTZ | n/s (up or down; 2.5 ULP) | impl unless RoundingModeRTE ( 2.5 ULP) | RNE | RNE ( ULP) |
| Subnormals (fp32) | may flush | flush / impl | may flush | may flush | impl unless DenormPreserve | preserved | impl |
| Case | Class | Source | Kernel | Rule | Cmp. |
|---|---|---|---|---|---|
| g_add_wrap | H1 | GLSL | x + 1 x with x near INT_MAX | GLSL 4.60 §4.1.3 WRAP | exact |
| g_mul_wrap | H1 | GLSL | (x * 2) / 2 == x | GLSL 4.60 §4.1.3 WRAP | exact |
| g_loop_wrap | H1 | GLSL | for (k = s; k s + 4; ++k) | GLSL 4.60 §4.1.3 WRAP | exact |
| h_add_wrap | H1 | HLSL | x + 1 x | A: D3D11.3 §22.12.1 iadd WRAP; B: DXIL add nsw | exact |
| h_mul_wrap | H1 | HLSL | (x * 2) / 2 == x | A: D3D11.3 §22.12.6 imul WRAP; B: DXIL mul nsw | exact |
| h_loop_wrap | H1 | HLSL | loop bound s + 4 | A: DXBC WRAP; B: DXIL add nsw | exact |
| Item | Registered prediction or plan | Corrected | Registered | Status |
| Suite | 66 cases by the selection rule | 56 with an obligation | 58 | corrected (A12) |
| H-R1, outcomes | every validated CrossTL-R output preserves the rule on every executed runtime | 5 divergences, all MoltenVK wrap | 5 | falsified |
| H-R1, bases | every match on the target’s rule or a construction, except recorded assumptions | fails for 18 of 102 | 15 of 109 | falsified |
| H-R2 | a source-dependent (class, target) cell under CrossTL | 2 of 26 cells | 2 of 31 | holds, narrowly |
| H-R3 | no accepted share changes by more than 1 point | at most points | same | holds |
| A4, CrossTL-R | no held-out divergence except MoltenVK wrap | 0 of 462 executions | 0 of 492 | holds |
| CrossTL | CrossTL-R | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Case | Ref | Metal | lvp | MVK | D3D | GL | WGSL | Metal | lvp | MVK | D3D | GL | WGSL |
| g_add_wrap | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| g_mul_wrap | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| g_loop_wrap | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| h_add_wrap † | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| h_mul_wrap † | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| CrossTL | CrossTL-R | ||||
| Family | Source | Executed | Diverged | Executed | Diverged |
| wrap | GLSL | 36 | 0 | 36 | 0 |
| mixed compare | GLSL | 30 | 12 | 36 | 0 |
| HLSL | 30 | 12 | 36 | 0 | |
| MSL | 30 | 12 | 30 | 0 | |
| CUDA | 30 | 12 | 30 | 0 | |
| CrossTL | CrossTL-R | |||||||||
| Class | Metal | SPIR-V | HLSL | GLSL | WGSL | Metal | SPIR-V | HLSL | GLSL | WGSL |
| H1 | ✗ | ✓ s | ✗ | ✓ | ✓ | ✓ | ✓ s | ✓ | ✓ | ✓ |
| H2 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||
| H3 | ✗ | ✓ | inv | ✓ | ✓ | ✓ | inv | |||
| H4 | ✓ | ✗ | ✓ | ✓ | inv | ✓ | ✓ | ✓ | ✓ | ✓ |
| H5 | ✗ | inv | ✓ | ✓ | R | |||||
| Source | Target | Units | CrossTL | SPIRV-Cross | naga (SPIR-V) | naga (GLSL) | Tint | Slang |
| GLSL | Metal | 654 | 58 | 87 † | 46 † | 30 † | 71 † | – |
| GLSL | HLSL | 654 | 65 | 82 † | 40 † | 27 † | 71 † | – |
| GLSL | GLSL | 654 | 69 | 81 † | – | – | 70 | – |
| GLSL | WGSL | 654 | 28 | – | 52 † | 32 | 71 † | – |
| HLSL | Metal | 429 | 24 | 89 † | 55 † | – | 78 † | 53 † |
| HLSL | GLSL | 429 | 37 | 88 † | – | – | 76 † | 93 † |
| Target | Validator | Cells | Output | Accepted | Fallback | Usable | % | 95 % CI (families) |
| Metal | Metal compiler | 2,358 | 1,760 | 772 | 42 | 727 | 30.8 | 28.2–33.3 |
| HLSL | DXC | 2,585 | 1,846 | 956 | 14 | 942 | 36.4 | 33.4–39.4 |
| GLSL | glslang | 2,419 | 1,581 | 986 | 0 | 984 | 40.7 | 37.5–43.5 |
| SPIR-V | spirv-val | 2,585 | 1,901 | 1,489 | 603 | 886 | 34.3 | 31.3–37.1 |
| WGSL | naga, Tint | 2,343 | 1,274 | 530 | 0 | 526 | 22.4 | 20.0–24.7 |
| GLSL ES | glslang (ES) | 1,579 | 1,324 | 796 | 0 | 796 | 50.4 | 47.1–53.8 |
| Language | Project | Units | Native | Commit | Licence | Note |
| GLSL | Willems Vulkan examples | 347 | 347 | 4ee2501c | MIT | |
| GLSL | Khronos Vulkan-Samples | 307 | 307 | 177edebf | Apache-2.0 | |
| HLSL | Willems Vulkan examples | 308 | 308 | 4ee2501c | MIT | |
| HLSL | Khronos Vulkan-Samples | 121 | 121 | 177edebf | Apache-2.0 | |
| Slang | Willems Vulkan examples | 173 | 170 | 4ee2501c | MIT | |
| Slang | Khronos Vulkan-Samples | 158 | 158 | 177edebf | Apache-2.0 |
| Strict import | Feasible cells, validated targets (%) | |||||||
| Source | Units | % | 95 % CI | Cells | Usable | Degr. | Invalid | Refused |
| GLSL | 654 | 99.4 | 98.4–99.8 w | 4,313 | 57.3 | 4.4 | 31.9 | 6.4 |
| HLSL | 429 | 65.5 | 62.5–68.5 | 2,833 | 35.1 | 7.2 | 38.9 | 18.8 |
| Slang | 328 | 99.4 | 97.8–99.8 w | 2,147 | 38.5 | 8.5 | 38.0 | 15.0 |
| SPIR-V | 344 | 98.8 | 97.0–99.5 w | 2,233 | 39.5 | 0.3 | 46.9 | 13.3 |
| Metal | 240 | 35.4 | 29.6–41.2 | 1,489 | 3.9 | 1.7 | 9.1 | 85.2 |
| Slice | Units | Families | Cells | Usable | Degraded | Invalid | Stopped |
| Sascha Willems, GLSL | 347 | 95 | 2,254 | 61 | 4 | 29 | 6 |
| Vulkan Samples, GLSL | 307 | 78 | 2,059 | 54 | 5 | 35 | 7 |
| Vulkan Samples, Slang | 158 | 48 | 1,033 | 49 | 6 | 33 | 12 |
| Sascha Willems, SPIR-V | 344 | 94 | 2,233 | 40 | 0 | 47 | 13 |
| Sascha Willems, HLSL | 308 | 80 | 2,032 | 36 | 7 | 40 | 18 |
| Vulkan Samples, HLSL | 121 | 41 | 801 | 33 | 9 | 37 | 21 |
| Registered run | Crash fixed | |||||||
| Target | Cells | CrossTL | CrossTL-R | Bound | Crashes | acc. | out. | |
| Metal | 2,358 | 32.7 | 32.6 | -0.17 | -0.39 | 57 | +0.00 | +0.00 |
| HLSL | 2,585 | 37.0 | 37.0 | +0.00 | -0.12 | 68 | +0.00 | -0.27 |
| GLSL | 2,419 | 40.8 | 40.7 | -0.04 | -0.20 | 68 | +0.00 | +0.00 |
| SPIR-V | 2,585 | 57.6 | 57.5 | -0.08 | -0.24 | 68 | +0.00 | +0.00 |
| WGSL | 2,343 | 22.6 | 22.5 | -0.13 | -0.39 | 68 | -0.13 | -0.47 |
| Polyfill call sites | Units marked | ||||
| Target | Source | Strongest | Ratio | Source | Strongest |
| Metal | 1,550 | 30,337 | 19.6 | 34 C | 92 C |
| HLSL | 4,070 | 32,229 | 7.9 | 0 | 0 |
| GLSL | 3,015 | 18,472 | 6.1 | 34 A | 92 A |
| GLSL ES | 3,015 | 18,472 | 6.1 | 34 A | 92 A |
| SPIR-V | 3,015 | 18,472 | 6.1 | 0 | 0 |
| Apple | CrossTL | CrossTL/MVK | GLSL/MVK | Bound | |||||
| Function | max | over | max | over | max | over | max | over | (ULP) |
| sin | 3.86 | 0 | 3.19 | 0 | 3.19 | 0 | 3.2 G | 4 | |
| cos | 3.95 | 0 | 3.15 | 0 | 3.15 | 0 | 2.1 G | 4 | |
| exp | 1.38 | 0 | 0.91 | 0 | 0.93 | 0 | 64.46 | 37 M | 4 |
| log | 0.86 | 0 | 1.97 | 0 | 2.81 | 0 | 3.1 M | 4 | |
| log2 ∗ | 1.80 | 0 | 1.80 | 0 | 3.2 M | 3.2 M | 4 | ||
| Median CPU time (ms) | ||||||
| Source | Units | Import | + GLSL | 90th pct. | Cens. | Slope (95 % CI) |
| GLSL | 654 | 2 | 41 | 6 | 0 | 0.66 (0.60–0.72) |
| HLSL | 429 | 6 | 41 | 14 | 0 | 0.81 (0.76–0.85) |
| Slang | 328 | 3 | 54 | 8 | 0 | 1.05 (0.98–1.13) |
| SPIR-V | 344 | 2 | 62 | 3 | 0 | 0.89 (0.86–0.92) |
| Metal | 240 | 705 | 1436 | 22,798 | 18 | 0.92 (0.68–1.19) |
| ID | Component | Defect |
|---|---|---|
| S1 | Metal emitter | Signed from GLSL and HLSL emitted as C++ operators, which Metal treats as undefined on overflow ( g_add_wrap ). |
| S2 | SPIR-V emitter | isnan becomes an ordered comparison of the constant 0.0 with itself, which is always false. Unknown functions ( isinf , transpose , inverse , __shfl_down_sync ) become a neutral value with a warning comment, which formatting can strip. The modules pass spirv-val ( m_isnan , g_isnan ). |
| S3 | SPIR-V emitter | In for (k = s; k < s + 4; ++k) from HLSL, k is initialised to 0 and only incremented, never tested, and the condition re-tests s < s + 4 , so the loop does not terminate unless s + 4 overflows ( h_loop_wrap ). |
| S4 | SPIR-V emitter | precise from HLSL is not lowered to NoContraction : the emitter recognises only the attribute form ( h_precise , MoltenVK). |
| S5 | WGSL emitter | precise is dropped without a diagnostic, although WGSL cannot forbid contraction. |
| S6 | SPIR-V emitter | 8- and 16-bit integers are widened to 32 bits without re-narrowing ( m_uchar_wrap ). |
Code Translation
Code translation converts programs between languages, frameworks, and representations while aiming to preserve behavior and sometimes improve safety or performance.
See all 41 papers