Zig Version
0.17.0-dev.262+76b001ed3b
Steps to Reproduce and Observed Behavior
NOTE: #35174 changes are needed to reproduce this.
Minimal Zig kernel:
// repro.zigconstgpu=@import("std").gpu;constN:u32=16;constin_buf=gpu.runtimeArray(f32,0,0,"in_buf");constout_buf=gpu.runtimeArray(f32,0,1,"out_buf");exportfnmain()callconv(.spirv_kernel)void{gpu.executionMode(main,.{.local_size=.{.x=1,.y=1,.z=1}});varacc:f32=0.0;vari:u32=0;while(i<N):(i+=1){acc+=in_buf.*.data[i];}out_buf.*.data[0]=acc;}
Equivalent GLSL for comparison:
// repro.comp
#version 450
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(set = 0, binding = 0) readonly buffer In { float data[]; } in_buf;
layout(set = 0, binding = 1) buffer Out { float data[]; } out_buf;
const uint N = 16;
void main() {
float acc = 0.0;
for (uint i = 0; i < N; i++) {
acc += in_buf.data[i];
}
out_buf.data[0] = acc;
}
Compile both and run through SPIRV-Cross to MSL:
zig build-obj -target spirv32-vulkan -fno-llvm -fno-lld -fstrip -OReleaseFast \
-mcpu=generic+variable_pointers -Mroot=repro.zig -femit-bin=zig.spv
glslangValidator -V repro.comp -o glsl.spv
spirv-cross --msl zig.spv > zig.msl
spirv-cross --msl glsl.spv > glsl.msl
Both .spv files validate clean (spirv-val).
The Zig SPIR-V emits the loop with the condition expressed as a OpSelectionMerge inside the loop body, with an integer block-ID OpPhi feeding a follow-up OpBranchConditional:
header: OpLoopMerge %merge %continue None
OpBranch %body
body: OpSelectionMerge %sel_merge None
OpBranchConditional %lt_16 %then %else
then: ... ; OpBranch %then_pad
then_pad: OpBranch %sel_merge
else: OpBranch %else_pad
else_pad: OpBranch %sel_merge
sel_merge: %60 = OpPhi %uint %uint_7 %then_pad %uint_5 %else_pad
OpBranch %dispatch
dispatch: OpBranchConditional %eq_7 %continue_path %merge
continue_path: OpBranch %continue
merge: %64 = OpPhi %uint %60 %dispatch
...
continue: OpBranch %header
SPIRV-Cross cannot recover this as a structured for and emits a state-machine. The resulting MSL is 86 lines vs glslang's 27, and the loop body is:
for (;;)
{
uint _60;
if (_32 < 16u)
{
_30 += _16._m0[_32];
_32++;
_60 = 7u;
}
else
{
_60 = 5u;
}
if (_60 == 7u)
{
continue;
}
else
{
_64 = _60;
break;
}
}
if (_64 == 5u)
{
_23._m0[0u] = _30;
return;
}
The same shape appears in HLSL/WGSL output from SPIRV-Cross.
Runtime impact (Apple M1, MoltenVK, Vulkan compute, tiled f32 GEMM 1024x1024 with TILE=16): Zig spirv32-vulkan ReleaseFast at ~75 GFLOP/s vs glslang at ~135 GFLOP/s through the same MoltenVK SPIR-V -> MSL path. Both kernels produce identical output (max relative error 0). The ~1.8x runtime gap matches the structural difference in the resulting MSL, not the difference in raw SPIR-V instruction count (Zig is ~1.6x larger).
Expected Behavior
For a while (cond) { body } with a constant trip count and no early exits, downstream consumers should be able to recover the loop as a structured for (or equivalent native loop) in their target language. glslang produces SPIR-V that SPIRV-Cross translates back to a one-line for loop; the Zig backend's output triggers a state-machine fallback even on this minimal example.
The two .spv files are attached so the SPIRV-Cross step can be run without building Zig.
### Zig Version
0.17.0-dev.262+76b001ed3b
### Steps to Reproduce and Observed Behavior
NOTE: #35174 changes are needed to reproduce this.
Minimal Zig kernel:
```zig
// repro.zig
const gpu = @import("std").gpu;
const N: u32 = 16;
const in_buf = gpu.runtimeArray(f32, 0, 0, "in_buf");
const out_buf = gpu.runtimeArray(f32, 0, 1, "out_buf");
export fn main() callconv(.spirv_kernel) void {
gpu.executionMode(main, .{ .local_size = .{ .x = 1, .y = 1, .z = 1 } });
var acc: f32 = 0.0;
var i: u32 = 0;
while (i < N) : (i += 1) {
acc += in_buf.*.data[i];
}
out_buf.*.data[0] = acc;
}
```
Equivalent GLSL for comparison:
```glsl
// repro.comp
#version 450
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(set = 0, binding = 0) readonly buffer In { float data[]; } in_buf;
layout(set = 0, binding = 1) buffer Out { float data[]; } out_buf;
const uint N = 16;
void main() {
float acc = 0.0;
for (uint i = 0; i < N; i++) {
acc += in_buf.data[i];
}
out_buf.data[0] = acc;
}
```
Compile both and run through SPIRV-Cross to MSL:
```
zig build-obj -target spirv32-vulkan -fno-llvm -fno-lld -fstrip -OReleaseFast \
-mcpu=generic+variable_pointers -Mroot=repro.zig -femit-bin=zig.spv
glslangValidator -V repro.comp -o glsl.spv
spirv-cross --msl zig.spv > zig.msl
spirv-cross --msl glsl.spv > glsl.msl
```
Both `.spv` files validate clean (`spirv-val`).
The Zig SPIR-V emits the loop with the condition expressed as a `OpSelectionMerge` *inside* the loop body, with an integer block-ID `OpPhi` feeding a follow-up `OpBranchConditional`:
```
header: OpLoopMerge %merge %continue None
OpBranch %body
body: OpSelectionMerge %sel_merge None
OpBranchConditional %lt_16 %then %else
then: ... ; OpBranch %then_pad
then_pad: OpBranch %sel_merge
else: OpBranch %else_pad
else_pad: OpBranch %sel_merge
sel_merge: %60 = OpPhi %uint %uint_7 %then_pad %uint_5 %else_pad
OpBranch %dispatch
dispatch: OpBranchConditional %eq_7 %continue_path %merge
continue_path: OpBranch %continue
merge: %64 = OpPhi %uint %60 %dispatch
...
continue: OpBranch %header
```
SPIRV-Cross cannot recover this as a structured `for` and emits a state-machine. The resulting MSL is 86 lines vs glslang's 27, and the loop body is:
```c
for (;;)
{
uint _60;
if (_32 < 16u)
{
_30 += _16._m0[_32];
_32++;
_60 = 7u;
}
else
{
_60 = 5u;
}
if (_60 == 7u)
{
continue;
}
else
{
_64 = _60;
break;
}
}
if (_64 == 5u)
{
_23._m0[0u] = _30;
return;
}
```
The same shape appears in HLSL/WGSL output from SPIRV-Cross.
Runtime impact (Apple M1, MoltenVK, Vulkan compute, tiled f32 GEMM 1024x1024 with TILE=16): Zig spirv32-vulkan ReleaseFast at ~75 GFLOP/s vs glslang at ~135 GFLOP/s through the same MoltenVK SPIR-V -> MSL path. Both kernels produce identical output (max relative error 0). The ~1.8x runtime gap matches the structural difference in the resulting MSL, not the difference in raw SPIR-V instruction count (Zig is ~1.6x larger).
### Expected Behavior
For a `while (cond) { body }` with a constant trip count and no early exits, downstream consumers should be able to recover the loop as a structured `for` (or equivalent native loop) in their target language. glslang produces SPIR-V that SPIRV-Cross translates back to a one-line `for` loop; the Zig backend's output triggers a state-machine fallback even on this minimal example.
The two `.spv` files are attached so the SPIRV-Cross step can be run without building Zig.