Skip to content

Commit f9929e0

Browse files
LostBeardclaude
andcommitted
SpawnDev.ILGPU 4.9.5-rc.8: Wasm multi-view body-struct kernel param decomp
WasmKernelFunctionGenerator.IsViewType was returning `true` for any StructureType whose first DirectField is AddressSpaceType — including multi-view containers like Tuvok's VorbisPacketDecodeStaticInputs (38 ArrayView fields) and any user struct-of-views. The dispatcher routed the param through the single-view path which only registered the FIRST view's buffer in uniqueBuffers; the remaining N-1 view-pointers in the serialized struct never got their wasm offsets written, so the kernel read fields 1..N-1 as offset 0 of V0's buffer plus the IR struct's per-field byte offset. Symptom on Tests21.BodyStruct_12ArrayViewInt_PerFieldDiagnostic: V0[0]=100000 (correct), V1..V11=100004 (= V0[4], because the IR field offset for field 1 was 16 bytes and the kernel divided by 4 to index into V0). Tuvok's Vorbis Wasm path was producing silently-wrong PCM for the same reason — fell back to v1 path because of this. Fix: count AddressSpaceType DirectFields and only return `true` if exactly one (real ArrayView/ArrayView1D). Multi-view structs flow through the scalar-struct serialization path which already correctly registers each view's buffer via ExtractBuffersFromStruct and writes each view-pointer to its own field offset. Verification: - Tests21.BodyStruct_12ArrayViewInt_CoalesceTest Wasm PASS - Tests21.BodyStruct_12ArrayViewInt_PerFieldDiagnostic Wasm PASS - Tests21.BodyStruct_MixedIntFloatCoalesceTest Wasm PASS - Tests21.BodyStruct_VariableLengthCoalesceTest Wasm PASS - All Tests21 cases pass on every backend except WebGL (still gated with sharp technical reason — task #26 tracks the WebGL per-field sampler decomposition workstream). Partial WebGL improvement: GLSLKernelFunctionGenerator.IsMultiDim got the same multi-view detection fix — only treats a struct as a view when NumFields matches a known ArrayView shape (3, 4, or 6). This is necessary for the WebGL fix but not sufficient — the GLSL kernel codegen still needs per-field sampler decomposition + struct-aware GetField + dispatcher binding multiple samplers per param. Samplers cannot be struct members in GLSL ES 3.0, so the current `uniform struct_X u_param2` emit is invalid for multi-view containers. What this unblocks: - SpawnDev.Codecs Vorbis Wasm path — VorbisPacketDecodeStaticInputs (36 ArrayView<int> + 2 ArrayView<double>) now works on Wasm. Tuvok's dual-path branch in VorbisAudioDecoderGpu.DecodePacketAsync can drop the v1 fallback arm entirely. - Opus SILK + CELT Wasm decoder primitives — same struct-of-ArrayView pattern, browser-clean from day one on Wasm. - Possibly some of Data's Wasm async-mode race cluster (DDPM, MoveNet, 5 Style transfers, SqueezeNet, ESPCN, DepthAnything). Some of these may have been hitting multi-view-decomp rather than a true async race. Recommend re-running ML pipeline + reference suite on Wasm. Four-package bundle: Fork stays at 2.0.4. No edits to the forked ILGPU/ tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d39f8af commit f9929e0

7 files changed

Lines changed: 227 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,64 @@
22

33
This file tracks notable changes per release. The README's "Recent Highlights" section links here for the full version history.
44

5+
## 4.9.5-rc.8 (2026-05-04) — Wasm multi-view body-struct kernel param decomp fix
6+
7+
### Fix: Wasm dispatcher loses N-1 view buffers when kernel takes a multi-ArrayView container struct
8+
9+
`WasmKernelFunctionGenerator.IsViewType` was returning `true` for any `StructureType` whose first `DirectField` is `AddressSpaceType`. That's correct for `ArrayView<T>` / `ArrayView1D<T,Stride>` (single view-pointer + metadata fields), but ALSO matched multi-view containers like Tuvok's `VorbisPacketDecodeStaticInputs` (38 ArrayView fields) and any user struct-of-views. The dispatcher then routed the param through the single-view path which only registers the FIRST view's buffer in `uniqueBuffers`; the remaining N-1 view-pointers in the serialized struct memory never got their wasm offsets written, so the kernel read fields 1..N-1 as offset 0 of V0's buffer plus the IR struct's per-field byte offset.
10+
11+
Symptom on `Tests21.BodyStruct_12ArrayViewInt_PerFieldDiagnostic`: `V0[0]=100000 (correct), V1..V11=100004 (= V0[4], because the IR field offset for field 1 was 16 bytes and the kernel divided by 4 to index into V0)`. Tuvok's Vorbis Wasm path was producing silently-wrong PCM for the same reason.
12+
13+
### Fix surface
14+
15+
```csharp
16+
// Before:
17+
if (structType.DirectFields.Length > 0
18+
&& structType.DirectFields[0] is AddressSpaceType)
19+
return true; // matches single-view AND multi-view
20+
21+
// After:
22+
int viewPtrCount = 0;
23+
foreach (var df in structType.DirectFields)
24+
if (df is AddressSpaceType) viewPtrCount++;
25+
if (viewPtrCount == 1
26+
&& structType.DirectFields.Length > 0
27+
&& structType.DirectFields[0] is AddressSpaceType)
28+
return true; // only single-view
29+
```
30+
31+
Multi-view structs (`viewPtrCount > 1`) now flow through the scalar-struct serialization path which already correctly registers each view's buffer via `ExtractBuffersFromStruct` and writes each view-pointer to its own field offset.
32+
33+
### Verification
34+
35+
- `Tests21.BodyStruct_12ArrayViewInt_CoalesceTest` Wasm: 256-element output matches the per-field reference sum across all 12 ArrayView fields. Was wrong on every output index pre-fix.
36+
- `Tests21.BodyStruct_12ArrayViewInt_PerFieldDiagnostic` Wasm: each output[f] = refData[f][0] correctly. Was 11/12 wrong pre-fix.
37+
- `Tests21.BodyStruct_MixedIntFloatCoalesceTest` Wasm: PASS.
38+
- `Tests21.BodyStruct_VariableLengthCoalesceTest` Wasm: PASS.
39+
- All Tests21 cases pass on every backend except WebGL (still gated with technical reason — task #26).
40+
41+
### What this unblocks
42+
43+
- **SpawnDev.Codecs Vorbis Wasm path**`VorbisPacketDecodeStaticInputs` (36 `ArrayView<int>` + 2 `ArrayView<double>`) now works on Wasm. Tuvok's dual-path branch in `VorbisAudioDecoderGpu.DecodePacketAsync` can drop the v1 fallback arm entirely.
44+
- **Opus SILK + CELT Wasm decoder primitives** — same struct-of-ArrayView pattern, browser-clean from day one on Wasm.
45+
- **Possibly some of Data's Wasm async-mode race cluster** — DDPM / MoveNet / 5 Style transfers / SqueezeNet / ESPCN / DepthAnything. Some of these may have been hitting the multi-view-decomp bug rather than (or in addition to) a true async race. Recommend re-running the ML pipeline + reference suite on Wasm after rc.8 lands.
46+
47+
### Partial WebGL improvement
48+
49+
`GLSLKernelFunctionGenerator.IsMultiDim` got the same multi-view detection fix — only treats a struct as a view when `NumFields` matches a known ArrayView shape (3, 4, or 6). This is necessary for the WebGL fix but not sufficient: the GLSL kernel codegen still needs per-field sampler decomposition + struct-aware GetField + dispatcher binding multiple samplers per param. Tracked as task #26. Tests21 cases remain gated on WebGL with sharp technical reason ("samplers cannot be struct members in GLSL ES 3.0").
50+
51+
### Files changed
52+
53+
- `SpawnDev.ILGPU/Wasm/Backend/WasmKernelFunctionGenerator.cs` — IsViewType multi-view detection
54+
- `SpawnDev.ILGPU/WebGL/Backend/GLSLKernelFunctionGenerator.cs` — IsMultiDim multi-view detection (partial WebGL fix)
55+
- `SpawnDev.ILGPU.Demo.Shared/UnitTests/BackendTestBase.Tests21.CoalesceBindings.cs` — added per-field diagnostic test
56+
- `SpawnDev.ILGPU.Demo/UnitTests/WasmTests.cs` — un-gated Tests21 cases (Wasm fix verified)
57+
- `SpawnDev.ILGPU.Demo/UnitTests/WebGLTests.cs` — kept Tests21 gated with sharp technical reason for the remaining WebGL infrastructure work
58+
59+
### Four-package bundle: Fork stays at 2.0.4
60+
61+
No edits to the forked `ILGPU/` tree. Only `SpawnDev.ILGPU/Wasm/*` + `SpawnDev.ILGPU/WebGL/*` changed.
62+
563
## 4.9.5-rc.7 (2026-05-04) — WebGL GLSL codegen: INT_MIN const + unsigned compare/shift
664

765
### Three GLSL codegen bugs fixed

SpawnDev.ILGPU.Demo.Shared/UnitTests/BackendTestBase.Tests21.CoalesceBindings.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,101 @@ static void ManyIntViewsKernel(Index1D idx, ArrayView<int> output, ManyIntViewsS
5656
+ s.V11[idx];
5757
}
5858

59+
// Diagnostic kernel: writes EACH field's [0] value to a separate output slot.
60+
// Reveals which field reads return the right buffer's data. If output[f] = refData[f][0]
61+
// for every f, the body-struct decomp is fine. If any output[f] is wrong, the kernel's
62+
// GetField for that f reads from a wrong offset / wrong buffer pointer.
63+
// Used to bisect the Wasm body-struct decomp many-field bug (task #16).
64+
static void ManyIntViewsDiagnosticKernel(Index1D _, ArrayView<int> output, ManyIntViewsStruct s)
65+
{
66+
output[0] = s.V0[0];
67+
output[1] = s.V1[0];
68+
output[2] = s.V2[0];
69+
output[3] = s.V3[0];
70+
output[4] = s.V4[0];
71+
output[5] = s.V5[0];
72+
output[6] = s.V6[0];
73+
output[7] = s.V7[0];
74+
output[8] = s.V8[0];
75+
output[9] = s.V9[0];
76+
output[10] = s.V10[0];
77+
output[11] = s.V11[0];
78+
}
79+
80+
[TestMethod]
81+
public async Task BodyStruct_12ArrayViewInt_PerFieldDiagnostic() => await RunEmulatedTest(async accelerator =>
82+
{
83+
const int len = 64; // small per-buffer length, only need [0]
84+
var refData = new int[12][];
85+
var bufs = new MemoryBuffer1D<int, Stride1D.Dense>[12];
86+
try
87+
{
88+
for (int f = 0; f < 12; f++)
89+
{
90+
refData[f] = new int[len];
91+
for (int i = 0; i < len; i++)
92+
refData[f][i] = (f + 1) * 100000 + i;
93+
bufs[f] = accelerator.Allocate1D(refData[f]);
94+
}
95+
var s = new ManyIntViewsStruct
96+
{
97+
V0 = bufs[0].View, V1 = bufs[1].View, V2 = bufs[2].View, V3 = bufs[3].View,
98+
V4 = bufs[4].View, V5 = bufs[5].View, V6 = bufs[6].View, V7 = bufs[7].View,
99+
V8 = bufs[8].View, V9 = bufs[9].View, V10 = bufs[10].View, V11 = bufs[11].View,
100+
};
101+
using var output = accelerator.Allocate1D<int>(12);
102+
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<int>, ManyIntViewsStruct>(ManyIntViewsDiagnosticKernel);
103+
kernel((Index1D)1, output.View, s);
104+
await accelerator.SynchronizeAsync();
105+
var result = await output.CopyToHostAsync<int>();
106+
107+
// Each output[f] should equal (f+1)*100000.
108+
var sb = new System.Text.StringBuilder();
109+
bool anyWrong = false;
110+
for (int f = 0; f < 12; f++)
111+
{
112+
int expected = (f + 1) * 100000;
113+
bool ok = result[f] == expected;
114+
if (!ok) anyWrong = true;
115+
sb.Append($"f{f}:{(ok ? "OK" : $"WRONG got={result[f]} expect={expected}")}; ");
116+
}
117+
if (anyWrong)
118+
{
119+
// Wasm-only: pull the IR-layout-vs-CLR-serialization diagnostic
120+
// accumulator. These strings record exactly what the host wrote
121+
// into struct memory and at what offsets, so a mismatch between
122+
// what the kernel reads and what the host wrote becomes visible.
123+
string wasmDiag = "";
124+
try
125+
{
126+
// Look up WasmAccelerator by name across all loaded assemblies.
127+
// Avoids a project-time reference to SpawnDev.ILGPU.Wasm internals.
128+
System.Type? accelType = null;
129+
foreach (var a in System.AppDomain.CurrentDomain.GetAssemblies())
130+
{
131+
var t = a.GetType("SpawnDev.ILGPU.Wasm.WasmAccelerator");
132+
if (t != null) { accelType = t; break; }
133+
}
134+
if (accelType != null)
135+
{
136+
var idxField = accelType.GetField("_lastImplicitIndexDebug",
137+
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
138+
var serField = accelType.GetField("_lastStructSerialDebug",
139+
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
140+
wasmDiag = " | implDbg=" + (idxField?.GetValue(null) ?? "")
141+
+ " | serDbg=" + (serField?.GetValue(null) ?? "");
142+
}
143+
}
144+
catch { /* not Wasm — debug accumulators aren't available */ }
145+
throw new Exception("Per-field diagnostic: " + sb.ToString() + wasmDiag);
146+
}
147+
}
148+
finally
149+
{
150+
for (int f = 0; f < 12; f++) bufs[f]?.Dispose();
151+
}
152+
});
153+
59154
[TestMethod]
60155
public async Task BodyStruct_12ArrayViewInt_CoalesceTest() => await RunEmulatedTest(async accelerator =>
61156
{

SpawnDev.ILGPU.Demo/UnitTests/WasmTests.cs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -382,22 +382,15 @@ public async Task WasmStructScanShuffleDiagTest() => await RunTest(async acceler
382382
throw new UnsupportedTestException("Wasm: Warp.Shuffle requires subgroup support");
383383

384384
// Body-struct ArrayView coalesce tests (added 2026-05-03 alongside the WebGPU
385-
// binding-count coalesce fix). The codegen-side coalesce is WebGPU-specific —
386-
// Wasm doesn't have a maxStorageBuffersPerShaderStage limit and these structs
387-
// would normally just bind one buffer per field. However, the Wasm body-struct
388-
// codegen has a pre-existing limitation that causes wrong values when a struct
389-
// has many ArrayView fields (only the first field reads back correctly). That
390-
// is a SEPARATE Wasm bug, NOT a regression from the WebGPU coalesce work; until
391-
// it is fixed independently, skip these tests here.
392-
[TestMethod]
393-
public new async Task BodyStruct_12ArrayViewInt_CoalesceTest() =>
394-
throw new UnsupportedTestException("Wasm: pre-existing body-struct decomp limitation with many ArrayView fields — separate fix needed");
395-
[TestMethod]
396-
public new async Task BodyStruct_MixedIntFloatCoalesceTest() =>
397-
throw new UnsupportedTestException("Wasm: pre-existing body-struct decomp limitation with many ArrayView fields — separate fix needed");
398-
[TestMethod]
399-
public new async Task BodyStruct_VariableLengthCoalesceTest() =>
400-
throw new UnsupportedTestException("Wasm: pre-existing body-struct decomp limitation with many ArrayView fields — separate fix needed");
385+
// binding-count coalesce fix). Wasm body-struct decomp many-field bug FIXED
386+
// 2026-05-04 in WasmKernelFunctionGenerator.IsViewType — was returning `true`
387+
// for any struct whose first DirectField is AddressSpaceType, including
388+
// multi-view containers like ManyIntViewsStruct (12 ArrayView<int>). Now
389+
// counts AddressSpaceType DirectFields and only treats single-view structs
390+
// (ArrayView<T>, ArrayView1D<T,Stride>) as views; multi-view containers go
391+
// through the scalar-struct serialization path which already correctly
392+
// registers each view's buffer via ExtractBuffersFromStruct. No tests
393+
// gated on Wasm.
401394

402395
// ═══════════════════════════════════════════════════════════════
403396
// HALF PRECISION — codegen wrong values (7). f16 promoted to f32 but

SpawnDev.ILGPU.Demo/UnitTests/WebGLTests.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,31 @@ public WebGLTests(IPortableCrypto crypto, SpawnDev.WebTorrent.WebTorrentClient w
108108
public new async Task ILGPUReduceHalfTest() =>
109109
throw new UnsupportedTestException("WebGL: accelerator.Reduce requires atomics in the cross-workgroup combine step; WebGL 2.0 vertex shaders have no atomic operations. See Docs/atomic-operations.md for the per-backend matrix.");
110110

111-
// Body-struct ArrayView coalesce tests (added 2026-05-03 alongside the WebGPU
112-
// binding-count coalesce fix). Tests fail on WebGL with "cannot convert from
113-
// uniform highp int to highp float" GLSL compile error and zeroed reads —
114-
// this is a pre-existing GLSL codegen + body-struct decomposition limitation,
115-
// NOT a regression from the WebGPU coalesce work.
111+
// Body-struct ArrayView coalesce tests — Wasm fix shipped 2026-05-04
112+
// (`WasmKernelFunctionGenerator.IsViewType`). WebGL needs separate
113+
// codegen infrastructure: a multi-view body-struct kernel parameter
114+
// must decompose into per-field sampler uniforms (one isampler2D per
115+
// ArrayView field) + per-field offset/length uniforms + a struct-aware
116+
// GetField that maps each field to its sampler. Currently the GLSL
117+
// codegen path emits `uniform struct_X u_param2` which is invalid in
118+
// GLSL ES 3.0 because samplers cannot be struct members. Same shape
119+
// as the WebGL "no atomics in vertex shaders" gate (Rule 1.a — the
120+
// capability is genuinely unavailable on this backend without a
121+
// separate multi-hour codegen change). Task #25 tracks the WebGL
122+
// multi-view body-struct codegen workstream. Until that lands, gating
123+
// these three cases on WebGL with a sharp technical reason.
116124
[TestMethod]
117125
public new async Task BodyStruct_12ArrayViewInt_CoalesceTest() =>
118-
throw new UnsupportedTestException("WebGL: pre-existing body-struct decomp limitation with many ArrayView fields — separate fix needed");
126+
throw new UnsupportedTestException("WebGL: multi-view body-struct kernel param needs per-field sampler decomposition + struct-aware GetField codegen (samplers can't be struct members in GLSL ES 3.0). Task #25 tracks the WebGL infrastructure work.");
119127
[TestMethod]
120128
public new async Task BodyStruct_MixedIntFloatCoalesceTest() =>
121-
throw new UnsupportedTestException("WebGL: pre-existing GLSL codegen int/float type-inference issue with many-field body structs — separate fix needed");
129+
throw new UnsupportedTestException("WebGL: multi-view body-struct kernel param needs per-field sampler decomposition. Task #25.");
122130
[TestMethod]
123131
public new async Task BodyStruct_VariableLengthCoalesceTest() =>
124-
throw new UnsupportedTestException("WebGL: pre-existing body-struct decomp limitation with many ArrayView fields — separate fix needed");
132+
throw new UnsupportedTestException("WebGL: multi-view body-struct kernel param needs per-field sampler decomposition. Task #25.");
133+
[TestMethod]
134+
public new async Task BodyStruct_12ArrayViewInt_PerFieldDiagnostic() =>
135+
throw new UnsupportedTestException("WebGL: multi-view body-struct kernel param needs per-field sampler decomposition. Task #25.");
125136

126137
// Tests23 bisection cases — both the LoopUnrolling Shl trip-count bug
127138
// (rc.6) and the WebGL signed-shift / signed-compare codegen issues are

0 commit comments

Comments
 (0)