feat(WebGPU): add WebGPU support to VolumePassFSQ#3526
Conversation
|
Draft to get some initial feedback |
cfbfc19 to
2c55dfe
Compare
|
@sankhesh mind doing an initial review ? |
|
@sankhesh any chance to get an initial review ? |
sankhesh
left a comment
There was a problem hiding this comment.
This is a big change. Took me a while to go through it. I've left some questions but overall, needs more documentation and testing.
| let nearestCoord = clamp( | ||
| vec3<i32>(floor(tpos.xyz * vec3<f32>(dims))), | ||
| vec3<i32>(0), | ||
| maxCoord |
There was a problem hiding this comment.
This does not match the exact texel-center convention used by filter sampling, especially near boundaries.
| result.x = getComponentValue(vTex, tpos + vec4<f32>(tstep.x, 0.0, 0.0, 1.0), vNum, component) - scalar; | ||
| result.y = getComponentValue(vTex, tpos + vec4<f32>(0.0, tstep.y, 0.0, 1.0), vNum, component) - scalar; | ||
| result.z = getComponentValue(vTex, tpos + vec4<f32>(0.0, 0.0, tstep.z, 1.0), vNum, component) - scalar; |
There was a problem hiding this comment.
I know the webgl mapper uses forward differences only. Did you try central differences here? I think that would produce better results.
| var visibility = 1.0; | ||
|
|
||
| var j: i32 = 0; | ||
| loop |
There was a problem hiding this comment.
This is a nested sampling loop that is known to have quite an overhead. Have you profiled this?
There was a problem hiding this comment.
| Config | WebGPU (this PR) | WebGL |
|---|---|---|
| LAO off | 18.7 ms/frame | 16.7 ms/frame (vsync capped) |
| LAO on, kernel 15x7 (defaults) | 37.7 ms | 31.3 ms |
| LAO on, kernel 32x16 (max) | 129.8 ms | 111.5 ms |
Yes, with AI help. The loop is only reached when the user enables localAmbientOcclusion on the volume property; with it off the function early-returns before the loop and the frame time equals the pre-LAO baseline (18.7 ms). With LAO at the default kernel (15 rays x 7 steps) the frame goes from 18.7 ms to 37.7 ms, and at the maximum kernel (32x16) to 129.8 ms. That is a 2.0x / 6.9x cost, which matches the existing OpenGL implementation of the same nested loop on the same scene (16.7 -> 31.3 ms and 111.5 ms, i.e. 1.9x / 6.7x before accounting for the vsync-capped baseline). So the overhead is real but opt-in, scales with the user-chosen kernel size, and is at parity with the WebGL backend rather than a WebGPU-specific regression. The inner loop also exits early on volume-bounds and on visibility saturation, so the worst case kernelSize x kernelRadius fetch count is rarely reached.
| forceNearestMask |= 1 << component; | ||
| } | ||
| } | ||
| componentInfoArray[vidx * 4 + 3] = forceNearestMask; |
There was a problem hiding this comment.
I don't follow this. Could you please explain?
There was a problem hiding this comment.
Each volume being rendered has an SSBO (shader storage buffer) that carries per volume parameters up to the GPU. componentInfoArray is a vec4<f32> per volume - 4 float slots:
- [0] = number of components
- [1] = independent components flag (1.0 / 0.0)
- [2] = color mix preset
- [3] = the force-nearest bitmask <- line 1771
What lines 1765–1771 do
let forceNearestMask = 0;
for (let component = 0; component < numComp; component++) {
if (vprop.getForceNearestInterpolation(component)) {
forceNearestMask |= 1 << component;
}
}
componentInfoArray[vidx * 4 + 3] = forceNearestMask;A volume can have up to 4 scalar components (e.g. R, G, B, A). Each component individually can be flagged to use nearest neighbor texture sampling instead of the default linear interpolation (ForceNearestInterpolation).
Rather than send 4 separate booleans, the loop packs them into a single integer bitmask, one bit per component:
- bit 0 set -> component 0 forces nearest
- bit 1 set -> component 1 forces nearest
- etc.
1 << component produces the bit for that component (1, 2, 4, 8), and |= ORs it into the accumulator. So if components 0 and 2 want nearest, the mask is 0b0101 = 5.
That single number is stored in the 4th slot of componentInfo. On the GPU, the shader (in getTextureValue) unpacks it - checking whether bit N is set - to decide per component whether to do a nearest fetch and override the interpolated value.
2c55dfe to
5c98ce5
Compare
5c98ce5 to
8180170
Compare
83aa0a3 to
3c8d0ee
Compare
Context
Results
Changes
PR and Code Checklist
npm run reformatto have correctly formatted codeTesting