Hardware path tracer: progressive + realtime modes (PT-1/2/3/3b)#92
Merged
Conversation
added 16 commits
July 13, 2026 15:58
Groundwork for docs/pt/pt-roadmap.md — no rendering yet, just the switch: - renderer.pt_mode (0 off / 1 progressive / 2 realtime), set_path_tracing, pt_supported (== hw_rt_enabled: same ray-query + TLAS requirement as Lumen's HW trace), pt_active. Deliberately no software fallback: a software path tracer is seconds per frame, worse than useless in any mode we would ship. - FFI bloom_set_path_tracing / bloom_path_tracing_supported (+ manifest). - TS setPathTracing / isPathTracingSupported in bloom/core. The roadmap fixes the design: one compute megakernel (wgpu 29 has inline ray query only — no RT pipelines, no SBT), primary hits from the G-buffer, Tier 2 via a concatenated geometry megabuffer rather than buffer binding arrays, Tier 3 capped at temporal+à-trous (no ML denoiser exists for wgpu), and bloom-reference as the acceptance oracle with its BRDF conventions inherited so images stay comparable number-to-number.
One compute megakernel (shaders/pt.rs) replaces the lit opaque scene colour when path tracing is active: G-buffer primaries, TLAS bounce + NEE shadow rays, card-albedo hit shading, progressive accumulation (mode 1) or EMA (mode 2). Sky pixels are never written and the pass sits before translucency, so raster sky/clouds and water composite exactly as before. SSGI/SSR skip their dispatches while PT owns the frame; compose routes SSR to the cleared RT. Accumulation resets compare an UNJITTERED view-projection - the tracked current_vp_matrix carries the TAA Halton nudge, which reads as camera motion every frame and pins progressive mode at 1 sample. BLOOM_PT / BLOOM_PT_DEBUG env seed mode + debug view for batch runs. Verified on AMD 760M (DX12+DXC): magenta probe, standard-Z depth, convergence between 17s and 50s stills, live F9 off-switch restores raster bit-clean at 46 fps. Ticket doc carries the full status.
Geometry megabuffer (concatenated Vertex3D + index words, per-instance windows in InstanceGiData.geo), barycentric-interpolated normals/UVs at ray hits, and textured hit albedo through a 256-slot texture binding array in its own bind group (white-padded; feature- and limit-gated with a card-albedo fallback variant). Roughness/metalness plumbed per instance for the upcoming GGX lobe. Root-cause fix: current_inv_vp_matrix is stored transposed relative to what WGSL M*v computes, so every ray the PT kernel generated since PT-1 collapsed into one degenerate bundle - transport traced garbage while the image stayed plausible (primaries come from the G-buffer). The uniform now uploads the transpose. Found via the new numeric-readback debug modes (BLOOM_PT_DEBUG=16/17 dump t/instance/prim and p0/depth to pt_trace_dump.txt); the tonemap-immune t-vs-G-buffer sanity view (debug 13) is now the mandatory first check for ray-gen changes. wgpu 29 DX12+DXC inline ray query itself verified correct with a standalone repro (fat strides, same-encoder build+dispatch, multiple queries, engine-shaped bind groups) - no naga patching needed. Tickets: docs/pt/PT-2-real-hit-shading.md (new) + PT-1 post-scriptum.
Port of bloom-reference sample_brdf: Heitz VNDF specular lobe + cosine-sampled Burley diffuse, lobe picked by Fresnel-at-normal weight, metal/dielectric split via f0 = mix(0.04, base_color, metal). Primary hits read metallic/roughness from the material G-buffer; bounce hits read the per-instance mat_params plumbed by the megabuffer commit. Diffuse-only NEE is scaled by (1 - metallic); specular NEE is a documented gap (spec reflections of sky/scene come from the sampled bounce itself). Physical-convention throughput (BRDF*cos/pdf) collapses to plain albedo for pure diffuse, so the pi-premultiplied light intensities and PT-1 brightness parity are unchanged.
…ly clamp Progressive mode (first F9) was designed for a static camera; in gameplay every camera move reset accumulation, so the screen showed raw 1-spp Monte Carlo noise through TSR - unusable. Interim behaviour until PT-3 lands the denoiser: - Mode 1 keeps the RASTER frame (with SSGI/SSR - new pt_owns_frame() gate distinct from pt_active()) while the camera moves, and skips the kernel dispatch entirely, so moving costs nothing. Hold still for ~half a second (8 samples) and the path-traced image dissolves in, refining toward ground truth. - The firefly cap now scales with accumulation depth (4 at 1 spp up to 32 converged): a lone bright GGX specular sample no longer reads as a white dot in realtime mode or during warmup. Verified: static title still converges identically; F9 cycle off->PROG->RT->off restores raster cleanly at raster fps.
RT mode (F9 x2) blended the last ~8 frames per SCREEN PIXEL with no reprojection, so any camera motion averaged different viewpoints into smear - the foggy-lens look. History now follows world points: the previous frame result is fetched through prev_vp (unjittered, transposed like inv_vp) into a ping-pong accumulation pair, validated by depth (accum.w carries the raw depth its surface wrote; mismatch or off-screen rejects history, alpha=1). Verified crisp mid-strafe via a held-PostMessage-key harness - held keys DO reach game input. Also degates screen-space AO while PT owns the frame (ssao_blur white-clears): PT computes real occlusion by tracing, and GTAO on top double-darkened every crevice - one reason converged PT looked barely different from raster. Remaining PT-3: a-trous spatial filter over the residual grain + half-res tracing for the fps budget.
…pick The GGX lobe was picked with the Fresnel-at-normal weight (~4% on dielectrics) and the estimator divides by that probability - but real Fresnel approaches 1 at grazing angles, so rare grazing specular picks came back amplified ~25x. The whole ground plane is grazing at distance: at 1-2 spp that reads as white salt across the frame. Picking by Fresnel at the actual view angle makes the divisor track the true specular energy (clamped [0.05, 0.95] so neither lobe boosts past ~20x). Realtime mode additionally clamps per-bounce weights to 4x and uses a FIXED radiance cap of 6 - its EMA window stays ~7 frames no matter how large the frame counter grows, so the count-scaled cap had quietly relaxed back to 32 and stopped protecting anything. Progressive mode keeps unclamped weights + the count-scaled cap (deep averages absorb real energy; bias not needed there). Verified mid-strafe: uniform low-contrast grain, no white dots.
Two 5x5 B3-spline a-trous iterations (step 1, then 2) over the temporally-blended radiance, edge-stopped by relative linearized depth (accum.w) and luminance. The main kernel now only feeds the history buffer in realtime mode; cs_mid filters accum->scratch and cs_final filters scratch->hdr, skipping sky texels (w=1 marker) so the raster sky survives. Sigma_luma 0.25, both dispatches in one compute pass. This addresses the user-visible boiling: per-frame 1-2 spp grain re-rolls every frame and reads as crawling white shimmer live even when stills look acceptable. Filtered mid-strafe capture is smooth with a faint dusting - the remaining lever is half-res tracing for frame rate (RT ~11 fps at 1080p internal on the 760M).
Realtime mode now traces on a half-resolution grid (4x fewer rays; ~11 -> ~20+ fps mid-motion at 1080p internal on the 760M) and reconstructs full resolution in the final a-trous pass via depth-guided joint-bilateral upsampling. The pieces that made it look GOOD and not just fast, each verified with mid-strafe captures: - Albedo demodulation: history stores IRRADIANCE (radiance divided by primary albedo); the final pass re-multiplies by the full-res G-buffer albedo. Texture detail stays crisp despite the smoothing. - The firefly cap moved to irradiance space (cap 4): capping radiance before the albedo divide let dark-albedo texels amplify outliers ~20x past every filter sigma - white 2x2 blocks. - Three a-trous iterations (steps 1, 2, 4) with an SVGF-style sigma schedule: the first iteration barely edge-stops on luminance (1-spp variance IS the noise), later ones tighten to preserve shading. - Loosened history-validation + filter depth tolerances: at half-res the reprojected texel quantizes to 2 full pixels, and tight tests rejected nearly every frame on depth-chaotic surfaces (grass), pinning them at 1 spp. - Sample phase pinned to (0,0): rotating it re-rolled the owner pixel every frame and broke history validation the same way. Progressive mode is untouched (full-res, unclamped, converging).
Two mechanisms let isolated bright texels survive the whole denoiser: (1) the a-trous edge-stopping weights are measured FROM the center texel, so when the center itself is the outlier it rejects all its neighbors and passes through every iteration untouched; (2) a fresh 1-spp spike lives on screen for a frame before any filtering can act. Fixes: the first filter iteration clamps the center to 1.5x its immediate neighborhood maximum before blending (isolated texels cannot exceed what any neighbor has seen), and the kernel clamps a new sample to 1.5x + 0.25 above VALID reprojected history (single-frame upward spikes are noise at 1-2 spp; darkening stays unclamped so appearing shadows propagate at full speed). Verified mid-strafe: faint sparse dusting remains, the sparkle field is gone. 22 fps at 1080p internal.
Realtime mode now samples the primary sun cone with interleaved gradient noise (scrolled per frame) instead of white PCG noise: neighboring pixels get well-distributed sample positions, so the a-trous kernel averages penumbra/foliage sun visibility nearly flat instead of leaving shimmering mid-frequency blotch. Progressive mode keeps white noise for unbiased convergence. History EMA deepened (alpha 0.15 -> 0.1) now that spike clamps bound the outliers. Numeric shimmer meter (two stills 1.5s apart, standing, center crop): RT mean |delta| 54.6 / sparkle-grade 16.7% vs raster baseline 46.6 / 14.5% - RT temporal churn is now within ~17% of what the raster pipeline itself produces (TAA jitter + animated water dominate both).
The remaining full-frame sparkling was not outliers: every pixel whose sun visibility is stochastic (grass blades, canopies - most of a 4K frame) re-rolled its sample each frame and oscillated around its mean forever; a 10-frame EMA bounds the amplitude but never settles it, and no spatial filter removes temporally-uncorrelated oscillation. Realtime mode now uses a FROZEN sequence: IGN sun samples at frame 0 and the bounce RNG seeded without the frame index. Every sample is deterministic per pixel, so the path tracer contributes zero frame-to-frame variance; the a-trous flattens the static dither into stable soft shadows. Progressive and the debug views keep rolling sequences for unbiased convergence. Grass-band shimmer meter (two stills 1.5s apart, standing): RT now 32.0 mean / 9.1% sparkle-grade vs the RASTER pipeline itself at 42.8 / 12.9% - realtime path tracing is temporally quieter than raster TAA.
RT mode direct sunlight now comes from the raster shadow cascades (manual 2x2 load-and-compare) instead of one traced cone ray per pixel: crisp deterministic shadows identical to raster, zero direct- light noise, and the traced rays are spent purely on indirect. This addressed the dominant realism complaints - blobby dithered shadows and stippled penumbra. Progressive mode keeps traced sun for reference-quality soft shadows. Also adds the sun GGX specular at the primary surface (both modes; masked by the same visibility) - without it every surface read as dead-matte under direct sun. Bring-up notes, paid in verification blood: - shadow_map.light_vps upload RAW (they are consumed as M*v by every existing WGSL user, unlike the camera matrices which need the transpose). Verified numerically via a new debug-19 NDC dump. - textureSampleCompareLevel from compute was bypassed for manual textureLoad compare during diagnosis; kept the manual version (2x2 PCF) since it is proven and equally fast at this tap count. - The a-trous passes now skip when a debug view is active - they were overwriting every debug output with stale-buffer garbage, which cost two false diagnoses (both prior black frames were the filter stomping the debug write, not the cascades failing). - Debug readback offsets now use trace-grid dims (was full-res - OOB copy validation panic in half-res mode).
The RT trace grid is now capped at ~0.5 Mpx (960x540) and the upsampler + owner-pixel mapping generalize from the hardcoded factor 2 to arbitrary trace-to-full ratios. Raising the raster render scale (0.5 -> 0.75 shipped in the shooter after re-measuring: 46 -> 30 fps raster, dramatically sharper at 4K) therefore sharpens the image without multiplying path-tracing cost. Progressive stays uncapped.
Replaces the frozen-seed workaround stack (frozen RNG, spike clamp, despeckle, fixed sigma schedule) with the real architecture: rolling samples, temporal moments + history length in a new ping-pong buffer pair, bilinear per-tap-validated reprojection, surface-flip write-through keyed to the texel footprint depth window, five variance-guided a-trous iterations with squared-weight variance propagation, and first-iteration feedback into colour history. Two bugs the frozen seed had been masking, both fixed: - tlas_version bump (every node transform = every gameplay frame) reset realtime accumulation to 1 spp permanently; the reset is now progressive-only. - prev_vp uploaded transposed: mat4_multiply products are already in WGSL M*v layout (unlike mat4_invert outputs), so reprojection was garbage under any camera motion since PT-3 M1. New numeric debug views 20-23 (history heat, variance, SVGF state dump, reprojection dump). Verified against converged PROG ground truth; shimmer meter: RT 37.6/15.9pct vs raster baseline 49.5/16.7pct - the path traced mode is now temporally quieter than raster. See docs/pt/PT-3b-svgf-denoiser.md. Claude-Session: https://claude.ai/code/session_018574BdCfSjdpNK3WLgx1hP
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (20)
📝 WalkthroughWalkthroughAdds progressive and realtime GPU path tracing with a WGSL megakernel, SVGF denoising, PT-2 geometry/material support, hardware capability gating, public APIs, render-graph integration, screen-space pass exclusions, and extensive PT documentation. ChangesGPU path tracing
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant TypeScript
participant NativeFFI
participant Renderer
participant PTCompute
participant SVGF
TypeScript->>NativeFFI: setPathTracing(mode)
NativeFFI->>Renderer: set_path_tracing(mode)
Renderer->>PTCompute: dispatch path-tracing megakernel
PTCompute->>Renderer: write accumulation and HDR scene color
Renderer->>SVGF: process realtime history
SVGF->>Renderer: write denoised PT output
Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A three-tier hardware path tracer for the DX12+DXC ray-query path, runnable on integrated GPUs (developed and verified on a Radeon 760M at 4K).
shaders/pt.rs,pt_pass.rs): G-buffer primaries, TLAS bounce rays, accumulating ping-pong history; graph node sits between hdr_scene and translucent so the raster sky/clouds/water survive. Raster is shown while the camera moves; PT dissolves in when still.Two long-lived bugs found and fixed along the way (post-mortems in
docs/pt/):current_inv_vp_matrixis stored transposed relative to WGSLM*v; composed (mat4_multiply) products are NOT. The reprojection VP was uploaded with the wrong convention and collapsed to a ~40-texel band - temporal accumulation had never actually worked until PT-3b.tlas_versionbumps on every node transform (= every gameplay frame), and its accumulation reset silently pinned realtime history at 1 spp. The reset is now progressive-only.Verification
Docs:
docs/pt/PT-1-progressive-megakernel.md,PT-2-real-hit-shading.md,PT-3b-svgf-denoiser.md.Shooter-side toggle: Bloom-Engine/shooter PR (F9 cycle +
--ptCLI flag + HUD tag).https://claude.ai/code/session_018574BdCfSjdpNK3WLgx1hP
Summary by CodeRabbit
New Features
Documentation