Skip to content

feat(WebGPU): add WebGPU support to VolumePassFSQ#3526

Draft
daker wants to merge 8 commits into
Kitware:masterfrom
daker:feat-webgpu-volumepassfsq
Draft

feat(WebGPU): add WebGPU support to VolumePassFSQ#3526
daker wants to merge 8 commits into
Kitware:masterfrom
daker:feat-webgpu-volumepassfsq

Conversation

@daker

@daker daker commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Context

Results

Changes

  • Documentation and TypeScript definitions were updated to match those changes

PR and Code Checklist

  • semantic-release commit messages
  • Run npm run reformat to have correctly formatted code

Testing

  • This change adds or fixes unit tests
  • Tested environment:
    • vtk.js:
    • OS:
    • Browser:

@daker daker marked this pull request as draft June 3, 2026 10:03
@daker

daker commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

Draft to get some initial feedback

@daker daker force-pushed the feat-webgpu-volumepassfsq branch 2 times, most recently from cfbfc19 to 2c55dfe Compare June 3, 2026 12:18
@daker

daker commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

@sankhesh mind doing an initial review ?

@daker

daker commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@sankhesh any chance to get an initial review ?

@sankhesh sankhesh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Sources/Rendering/WebGPU/VolumePassFSQ/index.js Outdated
Comment on lines +41 to +44
let nearestCoord = clamp(
vec3<i32>(floor(tpos.xyz * vec3<f32>(dims))),
vec3<i32>(0),
maxCoord

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not match the exact texel-center convention used by filter sampling, especially near boundaries.

Comment thread Sources/Rendering/WebGPU/VolumePassFSQ/index.js
Comment on lines +139 to +141
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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nested sampling loop that is known to have quite an overhead. Have you profiled this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Benchmark Script

Comment thread Sources/Rendering/WebGPU/VolumePassFSQ/index.js Outdated
Comment thread Sources/Rendering/WebGPU/VolumePassFSQ/index.js Outdated
forceNearestMask |= 1 << component;
}
}
componentInfoArray[vidx * 4 + 3] = forceNearestMask;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this. Could you please explain?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Sources/Rendering/WebGPU/VolumePassFSQ/index.js
Comment thread Sources/Rendering/WebGPU/VolumePassFSQ/index.js Outdated
@daker daker force-pushed the feat-webgpu-volumepassfsq branch from 2c55dfe to 5c98ce5 Compare July 7, 2026 08:37
@daker daker force-pushed the feat-webgpu-volumepassfsq branch from 5c98ce5 to 8180170 Compare July 7, 2026 09:30
@daker daker force-pushed the feat-webgpu-volumepassfsq branch from 83aa0a3 to 3c8d0ee Compare July 8, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants