Skip to content

Hardware path tracer: progressive + realtime modes (PT-1/2/3/3b)#92

Merged
proggeramlug merged 16 commits into
mainfrom
feat/path-tracer
Jul 14, 2026
Merged

Hardware path tracer: progressive + realtime modes (PT-1/2/3/3b)#92
proggeramlug merged 16 commits into
mainfrom
feat/path-tracer

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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).

  • PT-1 - progressive megakernel (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.
  • PT-2 - real hit shading: geometry megabuffer (interpolated normals/UVs), texture binding array (256 slots, own bind group), GGX VNDF + Burley BRDF, emissive + point-light NEE.
  • PT-3 - realtime mode: half-res budget-capped trace grid (960x540 decoupled from render scale), temporally reprojected history, depth-guided upsample with albedo re-modulation, hybrid sun (direct visibility from the raster shadow cascades - deterministic and noise-free - traced cone kept for progressive).
  • PT-3b - canonical SVGF denoiser: rolling samples, temporal moments + history length, bilinear per-tap-validated reprojection, surface-flip write-through (one surface's history per trace texel), five variance-guided a-trous iterations with squared-weight variance propagation, first-iteration feedback into colour history. Replaces the interim clamp/freeze workaround stack.

Two long-lived bugs found and fixed along the way (post-mortems in docs/pt/):

  • current_inv_vp_matrix is stored transposed relative to WGSL M*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_version bumps 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

  • Numeric readback debug modes (BLOOM_PT_DEBUG=16..23): trace state, SVGF state, reprojection dump (sub-texel exact under an orbiting camera).
  • Ground-truth comparison against converged progressive stills.
  • Shimmer meter (two stills 1.5 s apart, grass band): RT 37.6 mean / 15.9% sparkle vs raster baseline 49.5 / 16.7% - the path traced mode is temporally quieter than raster.
  • ~15 fps RT mid-motion at 0.75 render scale on the 760M; F9 off -> raster restores cleanly.

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 + --pt CLI flag + HUD tag).

https://claude.ai/code/session_018574BdCfSjdpNK3WLgx1hP

Summary by CodeRabbit

  • New Features

    • Added configurable GPU path tracing with progressive and realtime rendering modes.
    • Added public controls to enable path tracing and check hardware support.
    • Added improved surface shading, texture support, lighting, accumulation, and realtime denoising.
    • Added automatic fallback behavior when required hardware capabilities are unavailable.
    • Path tracing now bypasses conflicting screen-space effects while preserving scene composition.
  • Documentation

    • Added roadmap, implementation details, verification results, and known limitations for path-tracing tiers.

Ralph Kuepper 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
@proggeramlug proggeramlug merged commit 1f6f090 into main Jul 14, 2026
8 of 10 checks passed
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 409cd8e4-b82a-4177-a2f1-0fe2eedda232

📥 Commits

Reviewing files that changed from the base of the PR and between 95657ea and 78c8f4d.

⛔ Files ignored due to path filters (1)
  • native/windows/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • docs/pt/PT-1-progressive-megakernel.md
  • docs/pt/PT-2-real-hit-shading.md
  • docs/pt/PT-3b-svgf-denoiser.md
  • docs/pt/pt-roadmap.md
  • native/shared/src/attach.rs
  • native/shared/src/ffi_core/visual.rs
  • native/shared/src/renderer/formats.rs
  • native/shared/src/renderer/hiz.rs
  • native/shared/src/renderer/mod.rs
  • native/shared/src/renderer/postfx_chain.rs
  • native/shared/src/renderer/pt_pass.rs
  • native/shared/src/renderer/shaders/gi.rs
  • native/shared/src/renderer/shaders/mod.rs
  • native/shared/src/renderer/shaders/pt.rs
  • native/shared/src/renderer/shaders/ssgi.rs
  • native/shared/src/renderer/ssgi_pass.rs
  • native/shared/src/renderer/ssr_pass.rs
  • native/windows/src/lib.rs
  • package.json
  • src/core/index.ts

📝 Walkthrough

Walkthrough

Adds 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.

Changes

GPU path tracing

Layer / File(s) Summary
PT architecture and verification
docs/pt/*
Documents PT tiers, progressive and realtime behavior, SVGF processing, verification results, milestones, and known limitations.
Public controls and device capabilities
native/shared/src/attach.rs, native/windows/src/lib.rs, native/shared/src/ffi_core/visual.rs, package.json, src/core/index.ts
Adds path-tracing mode and support APIs, plus conditional texture-binding-array feature and limit negotiation.
Renderer state and PT resource wiring
native/shared/src/renderer/mod.rs, native/shared/src/renderer/formats.rs, native/shared/src/renderer/shaders/{mod,gi,ssgi}.rs
Adds PT state, pipelines, accumulation resources, geometry/material windows, shader layouts, initialization, invalidation, and render-graph scheduling.
Megakernel and realtime denoising
native/shared/src/renderer/pt_pass.rs, native/shared/src/renderer/shaders/pt.rs
Dispatches progressive or realtime path tracing, manages accumulation and reprojection, performs ray-query shading, applies SVGF à-trous filtering, and supports debug readback.
PT frame ownership and screen-space pass routing
native/shared/src/renderer/{hiz,ssgi_pass,ssr_pass,postfx_chain}.rs, native/shared/src/renderer/mod.rs
Skips screen-space effects during PT-owned frames and avoids composing stale SSR history while preserving fallback clears and PT ordering.

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
Loading

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/path-tracer

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant