impr: Optimize radiance cascade execution - #2914
Conversation
|
pkg.pr.new packages benchmark commit |
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.70, 1.41, 3.19, 4.40, 5.54, 8.16, 16.37, 16.03]
line [0.73, 1.37, 2.82, 4.36, 5.42, 6.94, 15.77, 18.24]
line [0.68, 1.40, 2.81, 4.64, 5.52, 7.61, 17.23, 18.79]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.26, 0.40, 0.54, 0.59, 0.78, 0.85, 1.03, 1.14]
line [0.24, 0.39, 0.49, 0.60, 0.81, 0.88, 1.03, 1.14]
line [0.24, 0.40, 0.51, 0.61, 0.81, 0.88, 1.00, 1.10]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.67, 1.53, 2.47, 4.97, 8.73, 17.95, 39.07, 79.87]
line [0.70, 1.64, 2.46, 4.71, 8.46, 18.01, 39.21, 78.43]
line [0.56, 1.49, 2.27, 4.70, 8.25, 17.94, 39.62, 79.89]
|
Bundle size comparison (
|
| 🟢 Decreased | ➖ Unchanged | 🔴 Increased | ❔ Unknown |
|---|---|---|---|
| 0 | 325 | 0 | 0 |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 263.09 kB ( |
| tgpu_initFromDevice.ts | 262.55 kB ( |
| tgpu_resolve.ts | 161.75 kB ( |
| tgpu_resolveWithContext.ts | 161.68 kB ( |
| tgpu_bindGroupLayout.ts | 62.32 kB ( |
| tgpu_mutableAccessor.ts | 57.04 kB ( |
| tgpu_accessor.ts | 57.04 kB ( |
| tgpu_privateVar.ts | 55.73 kB ( |
| tgpu_workgroupVar.ts | 55.73 kB ( |
| tgpu_const.ts | 55.15 kB ( |
| tgpu_lazy.ts | 54.95 kB ( |
| tgpu_fragmentFn.ts | 39.68 kB ( |
| tgpu_fn.ts | 39.62 kB ( |
| tgpu_vertexFn.ts | 39.50 kB ( |
| tgpu_computeFn.ts | 39.20 kB ( |
| tgpu_vertexLayout.ts | 28.33 kB ( |
| tgpu_comptime.ts | 15.93 kB ( |
| tgpu_unroll.ts | 1.75 kB ( |
| tgpu_slot.ts | 1.70 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
There was a problem hiding this comment.
ℹ️ Solid optimization, two verification gaps worth closing.
Reviewed changes
- Shared ray directions through workgroup memory (
cascades.ts): when the cascade layout allows it, the PREAVERAGE_RAY_COUNT directions are computed once per workgroup and broadcast via aworkgroupVar+workgroupBarrier(); the OOB early-return was correctly moved after the barrier. - Per-layer gating + pipeline caching (
runner.ts):useSharedRayDirectionsis allowed only when workgroup 8×8 blocks align inside probe cells (probesdivisible by 8), and cascade pipelines are now cached keyed byhasUpperCascade:useSharedRayDirections. I verified the shared path is arithmetically identical to the per-invocation fallback (threadlocalIndex0..3 covers the same 4 directions the loop iterates), and that out-of-bounds workgroups still source the shared writes from valid threads. - Prebuilt jump-flood pipelines + single compute pass (
jumpFlood.ts): builds one pipeline per offset (offset uniform frozen) rather than mutating a shared uniform, and records the whole init + flood + finalize run into one caller-owned compute pass. I checked the ping-pong buffer parity for the prebuilt flood passes and thefinalizeReadBGselection against the oldsourceIdxalternation — both are equivalent for power-of-two offset sequences. - Tests (
cascades.test.ts): snapshots updated for shared-dir emission, memoization extended, plus a new snapshot pinning theuseSharedRayDirections: falsefallback.
ℹ️ Single-pass ordering is the load-bearing assumption
The whole jump-flood run is now written into one encoder.beginComputePass()/end(), whereas previously each flood pass got its own encoder + submit, forcing the driver to flush each pass (including its storage writes) before the next dispatch. Without a real-GPU test, this depends on the WebGPU command buffer surfacing the write→read dependency between successive dispatchWorkgroups commands inside a single compute pass. Worth confirming on the backend/driver — the codegen/docs tests won't catch a pickle-write artifact.
Technical details
# Single-pass batching vs storage write/read ordering
## Affected sites
- packages/typegpu-sdf/src/jumpFlood.ts:426 (`run`) — copies init + every flood pass + finalize into one compute pass, one submit.
## Required outcome
- Confirm that a storage-texture written by flood pass *i* is observable to flood pass *i+1* when both are recorded into the same `computePass`/submission. Previously each pass was a separate submit which forced serialization.
## Suggested approach
- A functional test (or run on a real GPU and inspect the SDF output) that asserts batched `run()` output matches the non-batched reference. If the driver does not synchronize in-pass dispatches, split the passes (or insert explicit synchronization).ℹ️ No functional-equivalence test for the new paths
The added tests assert WGSL emission only. Nothing asserts that useSharedRayDirections: true produces identical output to the false fallback, and nothing compares the batched jump-flood result against the previous per-dispatch behavior. The shared-dir indexing and the batch ordering are exactly where an indexing or visibility slip would live, so a runtime A/B test is cheap insurance.
ℹ️ Nitpicks
cascades.ts:517builds the cache key with string interpolation; fine, but the three boolean/mergeModecomponents now read a bit tersely — consider a structured key if the specialization grows again.- The runner gate
runner.ts:336hardcodesCASCADE_WORKGROUP_DIMthrough the constant, which is good; no change needed.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
2eacea7 to
7d1de3c
Compare
There was a problem hiding this comment.
Pull request overview
This PR optimizes radiance cascade and jump-flood execution by reducing per-dispatch overhead (pipeline/bindgroup churn and repeated direction computation) and by enabling whole runs to be recorded into a single compute pass, optionally into a caller-owned command encoder.
Changes:
- Add an optional caller-owned
TgpuCommandEncoderpath for jump-flood execution and record the full algorithm into one compute pass. - Introduce an optional “shared ray directions” cascade specialization using workgroup memory, and cache cascade pipelines by specialization.
- Centralize cascade workgroup sizing via
CASCADE_WORKGROUP_DIMand update shader snapshots/tests to cover the new specialization + fallback.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/typegpu-sdf/src/jumpFlood.ts | Prebuild per-offset pipelines and record the entire jump-flood run into a single compute pass with optional external encoder support. |
| packages/typegpu-radiance-cascades/src/cascades.ts | Add CASCADE_WORKGROUP_DIM and a new useSharedRayDirections specialization that can use workgroup memory for ray directions. |
| packages/typegpu-radiance-cascades/src/runner.ts | Use the new workgroup dimension constant, compute when shared-ray-direction specialization is valid per layer, and cache pipelines by specialization. |
| packages/typegpu-radiance-cascades/tests/cascades.test.ts | Update shader-resolution snapshots for shared-ray-direction variants and add coverage for the fallback specialization. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const offsets: number[] = []; | ||
| for (let offset = maxRange; offset >= 1; offset = Math.floor(offset / 2)) { | ||
| offsets.push(offset); | ||
| } |

Built on top of #2617.