Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions docs/pt/PT-1-progressive-megakernel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# PT-1 — Progressive path-trace megakernel (Tier 1)

Parent: [pt-roadmap.md](pt-roadmap.md). Requires: hardware ray query (merged
in #91), TLAS + `InstanceGiData` + Mesh Card atlases (Lumen tickets 007b/013).

## Scope

One compute megakernel (`renderer/shaders/pt.rs`, pass in
`renderer/pt_pass.rs`) that replaces the lit scene colour when
`pt_active() && pt_mode == 1`:

1. Primary hit from the G-buffer (depth → world pos, normal, albedo,
rough/metal). Sky pixels (far depth) get sky radiance directly.
2. Bounce loop, max 8:
- **NEE sun**: sample the solar disc (half-angle 0.265°), one shadow ray.
Delta-light radiometry identical to the raster sun so brightness
matches. Sky misses exclude the disc (no double count).
- **NEE point lights**: pick one of the frame's lights uniformly,
shadow ray, contribution / pdf. Same falloff as raster.
- **Diffuse bounce**: cosine hemisphere. Hit shading = card-atlas albedo
(textured) or flat `InstanceGiData` albedo + emissive — exactly what
the Lumen HW probe trace reads at hits today.
- Russian roulette from bounce 3 on throughput luminance.
3. Accumulate into an rgba32float buffer
(`accum = (accum * n + sample) / (n + 1)`); write tonemappable HDR into
scene colour. Reset `n` when the view matrix moves beyond epsilon or
`SceneGraph::tlas_version` bumps.

While active: skip SSGI, SSR, GTAO dispatches (banked cost, and their
output would be overwritten). Shadow cascades keep rendering — the card
light pass needs them, and mode 0 must be switchable back per frame.

## Milestone gates (each = compile + screenshot)

- **M1**: kernel writes solid magenta gated on `pt_mode >= 1`. Proves
pipeline, bind groups, TLAS binding, dispatch, scene-colour write, F9.
- **M2**: sky + sun NEE + 1 diffuse bounce, accumulation working — 2 s vs
15 s screenshots must show convergence (less noise, not merely
different).
- **M3**: full scope above. Point lights visible at night-ish exposure;
colour bleed visible at the building base (the EN-023 acceptance
criterion, finally measurable).

## Acceptance

- Convergence: variance visibly ↓ between 2 s and 15 s stills.
- Parity: same camera + fixed exposure in `bloom-reference`
(`--spp 256 --bounces 8`) vs converged in-engine frame; `bloom-diff`
RMSE reported in the PR. Differences must be explainable by flat hit
normals / card-res albedo (Tier 2 closes those).
- Toggling F9 off restores the raster path with no residue.

## Status (2026-07-13, AMD 760M / DX12+DXC)

- **M1 passed**: `BLOOM_PT_DEBUG=5` magenta over the full frame, HUD +
translucent water compositing on top, no validation errors.
- Depth convention confirmed standard-Z (`BLOOM_PT_DEBUG=1`: near dark,
far bright) — `is_sky(depth >= 0.9999999)` is correct as written.
- **M2 passed**: full render at brightness parity with raster; static
title camera at ~17 s vs ~50 s shows clearly reduced grass/canopy
noise (convergence, not mere difference). Fresh-accumulation noise
visible as expected right after a camera cut.
- Live mode switching verified: F9 → RT (EMA) mid-game, F9 → off
restores the raster path bit-clean (SSGI/SSR resume, 13 → 46 fps).
- **M3 partially open**: point-light NEE + building-base colour bleed +
`bloom-reference` RMSE not yet measured (needs a lit world + fixed
exposure; folded into PT-2/PT-5 verification).

Implementation notes discovered on the way:

- `current_vp_matrix` carries the TAA Halton jitter (~1e-3 in the proj
Z-coupling slots). The accumulation-reset check MUST compare an
unjittered VP (`current_proj_matrix_unjittered × view`) or mode 1
pins at 1 sample forever. The *jittered* `inv_vp` still feeds the
kernel — primary rays must match the jittered G-buffer depth, and
accumulating across jitters is free AA.
- TAA runs downstream of PT, so converged PT output gets a second
temporal filter (soft distant foliage). Acceptable for Tier 1;
PT-3 owns the proper answer (bypass/replace TAA under PT).
- Ray queries treat leaf-card cutouts as solid quads on bounce rays
(slight over-darkening under canopies). Primary visibility comes
from the G-buffer so silhouettes are unaffected. PT-2 material at
hits can alpha-test via `RAY_FLAG_NO_OPAQUE` + candidate loop if it
proves visible.
- Batch verification: `BLOOM_PT`/`BLOOM_PT_DEBUG` env seed the renderer
directly. Careful with F12-capture scripts: the title screen treats
the posted F12 as "press any key" and starts the game.

## Post-scriptum (2026-07-13, PT-2 bring-up): the transposed inv_vp

PT-2's numeric readback (BLOOM_PT_DEBUG=16/17) revealed that EVERY ray
this kernel generated since PT-1 was degenerate: `current_inv_vp_matrix`
is stored transposed relative to what WGSL's `M * v` computes, so the
unprojection collapsed all primary rays into one bundle. Light transport
(sun shadow rays, bounces) traced garbage while the image still looked
plausible — primaries come from the G-buffer and direct NdotL carried
the shading. Fixed by uploading the transpose (pt_pass.rs). Lessons,
paid for in full:

- The house rule exists for a reason: every other pass unprojects via
linear-z + projection coefficients and never touches an inverted
matrix. The PT kernel was the first `inv_vp` consumer, and the first
casualty.
- "Looks plausible" is not a transport test. The M2 convergence gate
passed on top of broken rays because accumulation mechanics are
independent of ray correctness. The debug-13 t-vs-G-buffer sanity
view (green/red/blue) is now the mandatory first check for any
ray-generation change.
- HDR-scaled probe colours (×20-50 to defeat tonemapping) saturate
fract-band signals into uniform blobs — three investigation probes
lied because of this. When probes disagree with expectations twice,
switch to numeric readback (debug 16/17) immediately.
- wgpu 29 DX12+DXC inline ray query is fully correct (validated with a
standalone repro: fat vertex strides, same-encoder build+dispatch,
multiple queries, engine-shaped bind groups — all fine). The naga
helper-function codegen needs no patching.

## Known honest limits (by design, closed by PT-2)

Flat per-mesh hit normals; card-resolution albedo at hits; no specular
bounce; no glass; sky at miss is the analytic Lumen sky, not the LUT sky.
64 changes: 64 additions & 0 deletions docs/pt/PT-2-real-hit-shading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# PT-2 — Real hit shading (Tier 2)

Parent: [pt-roadmap.md](pt-roadmap.md). Builds on PT-1.

## Scope

Replace PT-1's flat-normal/card-albedo hit shading with real surface
data at every bounce:

1. **Geometry megabuffer** — every TLAS instance's `Vertex3D` + index
data concatenated into two storage buffers (raw f32/u32 words,
zero repack via bytemuck; nodes retain CPU vertices). Per-instance
window in `InstanceGiData.geo` = (vertex_base, index_base,
index_count, texture_idx); `index_count == 0` falls back to PT-1
shading. Rebuilt with the instance-data buffer (grow-only).
2. **Interpolated attributes** — `fetch_hit_attrs` reads the hit
triangle via `primitive_index`, interpolates normal + UV with
`barycentrics` (DXR convention: w=1-u-v weights v0), transforms the
normal by the ray query's `world_to_object` (v·M = (M⁻¹)ᵀ·n).
3. **Texture binding array** — `binding_array<texture_2d<f32>>` in its
own bind group (wgpu forbids binding arrays next to uniform
buffers), fixed 256 slots padded with the white texture (no
PARTIALLY_BOUND needed). Requires TEXTURE_BINDING_ARRAY +
SAMPLED_..._NON_UNIFORM_INDEXING **and**
`max_binding_array_elements_per_shader_stage` (defaults to 0 —
must be requested at device creation). Hit albedo =
`inst.albedo × pt_textures[geo.w]` sampled at the interpolated UV.
Without the features the kernel compiles a white-stub variant and
stays on card albedo.
4. `mat_params` (roughness/metalness) plumbed per instance — GGX
specular lobe still TODO (M3).

## Status (2026-07-13, AMD 760M / DX12+DXC 1.9)

- Megabuffer + windows verified end-to-end: numeric dump shows correct
per-pixel t/instance/prim; debug 6 shows smooth interpolated normals
(leaf cards each with their own direction, smooth terrain); debug 7
shows real leaf/bark/plaster textures at traced hits.
- Debug 13 sanity: traced t agrees with the G-buffer everywhere a
proxy matches the visible mesh; expected mismatches only (grass and
skinned characters are not in the TLAS; tree proxies are unrotated).
- Found + fixed the transposed `inv_vp` upload (see the post-scriptum
in PT-1's ticket) — this had silently broken all PT-1 transport.

## M3 — GGX (landed b146b6d)

`sample_brdf` ported: VNDF specular + Burley diffuse, Fresnel-weighted
lobe pick, metal/dielectric f0 split. Primary material from the
G-buffer material RT, bounce material from `mat_params`. Verified: the
45 s converged title render is clean (no fireflies, no NaN blowups),
with visibly deeper foliage shading than the pure-Lambert version.

## Open (rolls into PT-3/PT-5)

- Specular NEE (direct highlights from sun/point lights on smooth
surfaces) — bounce sampling covers reflections but not delta-light
highlights; diffuse NEE is scaled by (1 − metallic) meanwhile.
- Emissive triangles at hits already work via `albedo × emissive_luma`;
emissive-as-light-source sampling is PT-4 (ReSTIR) territory.
- Known gaps, accepted: skinned enemies not in the TLAS (no per-frame
BLAS path yet); tree GI proxies unrotated (bounce-grade accuracy
only); leaf-card cutouts opaque to rays.
- Point-light NEE + colour-bleed visual + bloom-reference RMSE (carried
from PT-1 M3) — needs a lit test world + fixed exposure.
93 changes: 93 additions & 0 deletions docs/pt/PT-3b-svgf-denoiser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# PT-3b — SVGF denoiser for the realtime mode

Status: **landed & verified** (2026-07-14, 760M / DX12+DXC).

The user-facing complaint this fixes: realtime mode looked like "a dirty
lens with small ice crystals on it" — a static, screen-glued grain
pattern. The round-7 frozen sample sequence produced exactly that by
design (a wrong-but-stable 1-spp estimate, spatially filtered), and it
turned out to be masking two load-bearing bugs that had disabled ALL
temporal accumulation since PT-3 M1.

## What the realtime mode is now (canonical SVGF, Schied et al. 2017)

- **Rolling RNG in every mode.** The frozen sequence is gone; unbiased
fresh samples each frame are what the temporal estimator needs.
- **Temporal accumulation with moments.** New moments ping-pong buffers
(bindings 18/19) carry `(mu1, mu2, history length, raw depth)` per
trace texel. Color and moments accumulate with `alpha =
max(1/N, 0.1)` (cumulative average while young, EMA once mature; 0.1
rather than the paper's 0.2 because the half-res 2-bounce sky lottery
is the dominant noise source and the deeper average halves the
residual mottle — direct sun is cascade-deterministic, so the slower
EMA only delays indirect changes).
- **Bilinear 2×2 reprojection with per-tap depth validation** (tight
relative tolerance, surface identity) replacing point sampling + the
old loose-tolerance workaround.
- **Surface-flip write-through.** At half res a trace texel's owner
pixel alternates between surfaces with TAA jitter (grass blade vs
ground). When no tap matches but the stored surface is still inside
the texel's current footprint depth window, the history is kept
VERBATIM and the sample dropped — one texel holds one surface's
history; the depth-guided upsampler routes texels to full-res pixels
by surface. Blending would leak blade lighting onto the ground
(gray-blue mottle); resetting would pin the texel at 1 spp (speckle).
Only a stored surface that left the footprint is a disocclusion.
- **Variance-guided à-trous** — five iterations (steps 1/2/4/8/16), B3
5×5 kernel, `sigma_l = 4·sqrt(gaussian3x3(variance))`, variance
filtered alongside with squared weights, depth-gradient edge stop.
Young history (< 4 frames) substitutes a spatial variance estimate
and floors it at 0.25 so disoccluded texels blur instead of surviving
as false edges.
- **First-iteration feedback:** iteration 1's output is copied back
over the accum buffer as next frame's color history (moments stay
raw) — the SVGF detail that makes the loop stable at 1 spp.
- Removed: history spike clamp, despeckle max-clamp, fixed sigma
schedule, frozen IGN. Kept (documented deviation): a single
irradiance-space firefly clamp at 4.0, as reference implementations
do.

## The two masked bugs (both invisible under the frozen seed)

1. **`tlas_version` reset nuked realtime history every frame.**
`pt_accum_count` was zeroed whenever the TLAS version changed — and
it changes on every node transform, i.e. every gameplay frame.
Correct scoping: full reset for progressive only; realtime's
per-texel depth validation already invalidates exactly what changed.
Found via the debug-20 history-length view (black everywhere).

2. **`prev_vp` was uploaded transposed — reprojection was garbage under
any camera motion.** The engine has TWO matrix storage conventions:
`mat4_invert` outputs land transposed relative to WGSL `M*v` (hence
inv_vp's transpose-at-upload), while `mat4_multiply` products are
already in `M*v` layout (the shadow cascade VPs upload raw for the
same reason). Transposing the composed prev-VP collapsed every
reprojection into a ~40-texel band at screen centre with a
nonsensical depth. Found via the debug-23 numeric dump; after the
fix the dump shows sub-texel exact reprojection (pos tracks column
1:1, depth pair agrees to 0.1%) with the title camera orbiting.

## Verification

- Debug views: 20 = history length heat, 21 = variance ×10,
22 = numeric dump (n / variance / tap mass / depth),
23 = numeric dump of reprojected position + depth pair.
All dump through `pt_trace_dump.txt` (BLOOM_PT_DEBUG env).
- Ground truth: converged PROG stills (25 s static) — smooth dark
ground under grass; RT now matches its structure (the earlier
gray-blue mottle was cross-surface leakage, not signal).
- Shimmer meter (two stills 1.5 s apart, grass band):
**RT 37.6 mean / 15.9 % sparkle vs raster 49.5 / 16.7 %** — the path
traced mode is temporally QUIETER than the raster baseline.
- Cost: ~15 fps RT vs ~17 before at 0.75 render scale (moments buffers
+ two extra wavelet iterations + feedback copy).

## Notes / open

- Known hybrid-sun tradeoff: cascade shadows cannot resolve grass
micro-shadows, so RT ground under grass is brighter and flatter than
the traced-sun PROG reference. Deliberate (stability over contact
detail); traced-sun-in-RT would be a quality toggle, not a fix.
- The write-through's footprint window compares current-frame linear
depth against a stored prev-clip depth; under fast motion it degrades
toward disocclusion-reset, which is the safe direction.
Loading
Loading