The method for diagnosing a Titan renderer artifact. Stable; the empirical record lives in
docs/rendering/RENDERING_BUG_HISTORY.md and the bench/instrument detail in references/.
These are hard dependencies. Skipping any = ungrounded work. The provenance rule from
realtime-rendering-3d is load-bearing for IP posture (never paste proprietary source into committed files).
2b. Rule out global/shared passes before trusting a model- or branch-specific hypothesis. A
symptom reported against one lighting model or one recent branch is not evidence the cause lives
there — Pass::Tonemap and other model-agnostic post-process passes run downstream of every
model/cell. Confirmed method (2026-07-15): an FFT of a suspect near-black ROI showed a strong
periodic (176× mean-magnitude) non-DC spike — genuine ordered-pattern evidence, not an impression
— that stayed byte-identical across two named suspects (a feature branch AND the commit either
side of a merge under investigation) before it was traced to the final tonemap's ordered dither.
When a cross-arm parity check (same command, same metric, all arms) comes back byte-identical,
that is itself the confirming result — it falsifies both suspects and redirects to shared
infrastructure, it is not an inconclusive non-finding to explain away.
2c. For any dither/quantization artifact, check the color-space ordering before the amplitude.
The first fix attempt for the 2026-07-15 near-black speckle (tapering dither amplitude by luma)
was a plausible-looking but WRONG mechanism — the real defect (found by a literature survey, not
more measurement on the same axis) was that the dither was added in linear space before the GPU's
automatic sRGB encode instead of after. Near black the sRGB EOTF's slope (~12.92) amplifies a
linear-space perturbation by that same factor once the hardware re-encodes it — a ±0.5/255
dither becomes ~±6.4/255 in the visible output. Before proposing an amplitude-shaping fix for
any dither/banding artifact on an auto-*Srgb render target, verify (a) the target format via
Renderer3d::surface_format/surface_format_linear and which view the pass actually writes
(some passes use an alternate linear view deliberately — egui does), and (b) whether the dither
is applied before or after the linear→sRGB encode. Getting the color space right is orthogonal to
getting the amplitude right, and a survey of the literature (Bevy/Godot/canonical GDC talks) will
surface the space-ordering rule long before a from-scratch amplitude experiment would.
-
Growing black regions / a whole-frame collapse to black under a MULTI-light stochastic sampler,
where the SAME regime is fine with only 1 light = an unclamped WRS unbiased-estimator weight
(W_out = w_running/picked_w), NOT a TAA/denoiser bug per se. With 1 candidate, W_out is
trivially 1.0 (zero variance); with ≥2, it has real variance and can spike. If ONE consumer of
a WRS/SampleRecord producer clamps the weight before use (a firefly cap) and a SIBLING consumer
of the SAME producer does not, the unclamped sibling is the leak (2026-07-09, Lattice-Mid:
lattice_shade.wgsl clamps to MAX_SHADING_WEIGHT; froxel_fog_inject.wgsl's lattice_active
branch did not — NaN entered froxel_scatter/froxel_integrated, not the direct shade output).
TAA (or any temporal accumulator downstream) will look like the culprit — --taa false "fixes"
it, every TAA feature toggle fails to fix it — because it faithfully amplifies/exposes an
already-corrupted hdr_intermediate, not because TAA owns the bug. Isolate by: (1) tapping the
DIRECT lighting-model output (e.g. lattice_radiance_raw) for NaN/Inf — clean there means the
runaway is in a DIFFERENT term composited into hdr_intermediate (fog/SSR/GI); (2) tapping every
candidate composited term (fog-volume/froxel taps if present) for np.isnan().sum() directly —
Reinhard-tonemapped PPM previews alone cannot distinguish "legitimately bright" from "NaN/Inf"
(both saturate to 255); use TITAN_DUMP_LINEAR=1 for the raw .bin and check for NaN explicitly.
A spatial (not just global-mean) check — a 16×16 grid occupancy map compared against the
same-window 1-light control — reveals whether the corruption is a directional/spreading front
(consistent with a per-voxel/per-cell producer) rather than uniform dimming, which a global
mean/std metric alone cannot distinguish.
-
pre-TAA tap is the denoiser-vs-estimator discriminator. --capture-target pre-taa taps the
radiance_raw edge after StratumComposite, BEFORE TAA. If the artifact is ABSENT pre-TAA →
TAA/denoiser owns it (e.g. 2026-06-20 static near-wall dark annulus = TAA YCoCg neighbourhood-stats
corruption by a bright-core outlier; fix = depth/normal-gate the 3×3 stats, Bitterli 2020 §5). If
the artifact is PRESENT pre-TAA → it is upstream (estimator / shadow / shading). Run this tap EARLY;
it splits the pipeline in one shot and kills whole hypothesis branches.
-
Concentric RING shadows / a dark-then-light halo as a light nears or passes THROUGH thin geometry
= SDF sphere-march thin-occluder overstep, NOT reuse bias and NOT TAA. shadow_sdf_global.wgsl
steps by max(d, MIN_STEP); the nearest-seed distance reports ≈voxel_size one voxel from a
~1-voxel wall, so a phase-dependent step jumps clean over it → iso-phase rings (2026-06-20,
FIXED c5af38b8). Isolate: TITAN_STRATUM_SHADOW_TERM_DISABLED=1 (rings gone ⇒ shadow term),
--denoiser-tier low (no SDF march ⇒ gone), TITAN_SHADOW_SDF_HIT_THRESHOLD_M sweep (rings→leak
is the overstep tell). Fix = MAX_STEP clamp (clamp(max(d,MIN_STEP), MIN_STEP, vox*0.5), Nyquist /
Quilez 2010) + forward-only golden-ratio-dithered march start. A dark annulus is the textbook
ReSTIR biased-reuse signature (Bitterli 2020 §4), which makes reuse the seductive-but-wrong lead —
the REGIR_DIRECT=0 + --stratum-spatial-reuse off A/Bs falsified it here. Run the falsifying
toggle before committing to the literature's first match.
-
--wall-pierce (probe) is the repro for through-wall shadow artifacts: camera held head-on 4 m
from the wall, primary light swept in Z straight through the wall plane (crosses at the timeline
midpoint). Decouples light-through-geometry from camera motion — far cleaner than the orbiting fleet.
-
Area-light A/B arms (probe, 2026-07-16): --area-light swaps the scripted primary for a 2×1 m
rect AreaLight (same color/intensity/range); --area-flux-point swaps it for the EQUAL-TOTAL-FLUX
point arm (I = A·L/4 = LIGHT_INTENSITY/2 — one-sided Lambertian rect Φ = π·A·L vs isotropic
4π·I). Together they are the LTC rect-vs-point comparison instrument on --cell deferred-lattice
(P2.5 acceptance evidence: broadened highlight px>0.7 luma 97k vs 12k, zero non-finite tap texels).
-
Through-wall LIGHT LEAK (floor lit where a wall should shadow it) = the screen→SDF FORWARD-SKIP
HANDOFF. shadow_screen.wgsl covers [0,2 m] then hands start_dist_m to shadow_sdf_global.wgsl,
which skipped forward by HANDOFF_EPSILON_SCALE×voxel → flank dead-zone (skip oversteps real
crossings) + center no-op (dist_to_light<2 m → SDF starts at the light → 0 steps). FIX = drop the
forward-skip, full-march from a receiver normal-offset origin (render_pos + n×voxel) — the
normal-offset is mandatory or the bare full-march self-occludes the whole floor (RAY_BIAS 0.05 m <
0.25 m voxel). Canon = Wright 2015 (R1/R3). MERGED 649c1cb3 (PR #25,
perceptual gate ACCEPTED 2026-06-21 — the user accepted that R1+R3
reopens the grazing-corner over-occlusion; signed-SDF canonical fix is a future milestone).
The five canon-ranged march params are now
tier-gated RenderQuality knobs (MAX_MARCH_STEPS/HIT_THRESHOLD_M/MIN_STEP_M/
SDF_NORMAL_OFFSET_VOXELS/RAY_BIAS_M); TITAN_SHADOW_SDF_* envs override on top.
-
SDF corner/grazing OVER-occlusion (false dark band; SDF-off recovers it) is an UNSIGNED-SDF DATA
artifact, NOT a march-parameter bug. Titan's unsigned SDF + positive HIT_THRESHOLD false-hits
grazing rays (parallel-near a wall, d>0 but <ε); Wright canon uses a SIGNED SDF with sign-crossing
termination (d≤0 = inside) so grazing never false-hits. Tell: ALL march-side single-signal
discriminators (distance skip, near-hit rejection, grazing normal-vs-ray) fail to separate the false
corner hit from a real wall crossing — they are geometrically identical in the same cascade. That
identity IS the signature that the fix is in the DATA (signed SDF / mesh-voxelized seeding), not the
march. Don't chase distance/angle levers once they're shown to overlap.
-
RETIRED (2026-07-11, P3.5 Task 5, commit dddbc81e): the 12-iteration wall-normal/dir-direction
escape loop in march_shadow (shadow_visibility_sdf.wgsl) is GONE — do not look for it, and do not
re-propose an escape-walk fix for a self-occlusion artifact at a concave corner. It was retired in
favor of a per-tier self-occlusion START-GATE: NEAR_SELF_SKIP_VOXELS=2.0 (near-field per-mesh
candidates) and GLOBAL_SELF_SKIP_VOXELS=4.0 (the global cascade, added by Task 5c after retiring
the loop reopened junction false hits). If a wall-floor-junction false-occlusion artifact reappears,
the first thing to check is whether the false-hit t clusters near K × cascade_voxel_size_m for
either gate (round 22's decisive signature: the false-hit population RE-CLUSTERS at whatever the
current gate threshold is, rather than vanishing at a fixed world-space radius — a re-arming
artifact, not a fixed-radius contamination footprint a bigger constant safely clears once). A
coordinate-space methodology trap cost 3 rounds (18, and implicitly 6/8/9) chasing a phantom
"JFA +X-edge seed asymmetry": any diagnostic script that decodes the global-SDF cascade's
origin/world_min fields must first subtract camera_world_pos from a raw world-space query
position before doing origin/voxel-index math — those fields are RENDER-SPACE (camera-relative
under LWC), not absolute world space (FrameUniforms's own doc comment says so). Feeding a raw
world position directly into that math produces a self-consistent-looking but WRONG decode (a
"bogus self-seed 1.5 m below the floor" that was actually a correct floor-surface seed at world
Y=0.0). The canonical corrected decoder now lives at
titan-dumps/global-sdf-decode/global_sdf_decode.py (gitignore-negated like edge-width-mtf/) —
use it, don't re-derive the render-space subtraction from scratch. titan-render-bench-probe's
TITAN_BENCH_FREEZE_T=<seconds> env freezes the camera at an exact timeline pose for reproducible
per-texel cascade inspection across frames — useful whenever a static, fully-controlled camera pose
is needed for cascade/GI-cache debugging, not just this investigation.
-
A period-4/period-8 deterministic flicker (autocorrelation lag2 strongly negative, lag4 strongly
positive, lag8 near-max positive, odd lags ~0) under an 8-sample Halton(2,3) TAA jitter cycle IS a
known sequence property, not a coincidental phase alignment — verify with a quick offline numeric
check (no GPU needed): compute the raw halton_2_3 x-sequence for n=0..7, take consecutive
|Δx| jump magnitudes, and autocorrelate THAT series the same way you autocorrelate the measured
|ΔY| series. The base-2 van der Corput bit-reversal recurrence has a genuine period-4 substructure
in its per-step size (a carry to a coarser bit every 4th sample produces a bigger jump than the
alternating pattern between the others) — this numeric control reproduces the SAME lag-2/lag-4/lag-8
sign pattern as the observed artifact (2026-07-13, confirmed to 2-3 decimal places) and is strong,
cheap, independent corroboration before or alongside a GPU A/B. Isolate the mechanism by tapping
the shading-model's OWN direct-light output BEFORE any denoiser/composite/TAA (e.g.
lattice_diffuse_raw/specular_raw, not the swapchain) and freezing the jitter (TITAN_RASTER_JITTER =0) AT THAT TAP — a swapchain-level jitter-freeze test is confounded by GI/denoiser/TAA reacting
to the now-static upstream signal with their own residual mechanics (2026-07-12/13 round's "raster
jitter REFUTED" reading was exactly this confound; the direct-light tap resolved it to a clean
bit-exact zero-delta proof). Candidate fix direction: a low-discrepancy sequence with a CONSTANT
per-step increment (e.g. the R2/Roberts-2018 additive-recurrence sequence, public-domain, not
proprietary) in place of Halton removes the jump-size periodicity by construction — tested 2026-07-13,
real but partial swapchain-level improvement (lag8 ~77% reduced), not yet a full noise-floor collapse;
see RENDERING_BUG_HISTORY.md 2026-07-13 entry for the full A/B. 2026-07-14 follow-up, load-bearing
for any future TAA jitter-sequence decision: R2 is NOT a canon fork. The realtime-rendering-3d
skill's own taa.md Pairing rules pin "pair the [jitter] period to 1/α" and name R2 as the
canon-sanctioned choice when the accumulation window needs to be longer than an 8-tap Halton cycle
can cover. Before treating a jitter-sequence swap as a divergence, MEASURE the actual 1/α at the
artifact's ROI from taa.wgsl's own blend = clamp(1/(1+max(luma_curr,luma_hist)), 0.04, 0.5)
formula (decode luma from an existing HDR tap, e.g. lattice_radiance_raw .bin, no new instrument
needed) — a moderate-luma ROI (luma < 1.0) pins blend at the CEILING (0.5, N_eff=2 frames), not
the floor, which is the OPPOSITE of "long window at a bright ROI" and means canon's own rule already
calls for a period far shorter than 8. Also falsified this round: widening the accumulation window
alone (Halton unchanged) does NOT collapse a period-N jitter signature — it leaves the target lag
unchanged and smears correlation across every OTHER lag instead (the textbook broad-spectrum ghosting
signature), because a linear EMA filter preserves an exactly-periodic input's normalized autocorrelation
shape regardless of window length; only changing the source sequence's own per-step jump-size
uniformity moves the periodic lag. See RENDERING_BUG_HISTORY.md 2026-07-14 entry for the full
canon-check text, the falsifying A/B, and the yaw/dolly motion-regime validation.
-
A per-frame-parity (period-2/4/8) content oscillation at a FIXED screen/probe position, surviving
every reprojection/resampling/world-cache-content override, points at a Hammersley/van-der-Corput
PLACEMENT jitter, not a lookup-indexing formula (2026-07-13, round 4 of the Lattice top-left-wall
reflection asymmetry). Before assuming a periodic signature lives in a spatial lookup's indexing math,
check whether that lookup's inputs even CAN carry a frame-dependent term under the repro's camera
motion (a static-camera repro + TAA jitter confirmed to perturb only the projection matrix, never
camera_pos, is a hard structural exclusion, no GPU needed). If excluded, the next suspect is any
OTHER per-frame jitter feeding the position used by that lookup — e.g. a screen-probe/reservoir
PLACEMENT pass's own canon-sanctioned per-tile jitter (Wright 2021 S37's "temporally jitter placement
grid" for screen-probe GI) — at diagnosis time computed via a base-2 bit-reversal Hammersley(8), same
sequence family as Halton; mark_uniform now reads a CPU-computed R2 offset from the place UBO
(06c7169d), so this specific instance is fixed and the pattern applies to any remaining short
bit-reversal jitter consumer. Freezing candidate content (e.g. TITAN_LATTICE_WC_FREEZE_UPDATE-style:
clamp the frame index fed to the SUSPECT pass's UBO) and separately freezing the PLACEMENT jitter
(clamp the frame index feeding the jittered pass instead) discriminates the two — content-freeze left
the signature byte-identical, jitter-freeze collapsed it to a flat noise floor. This is the SAME
root-defect class (non-uniform per-step jump size in a bit-reversal low-discrepancy sequence) as the
period-4 TAA-jitter entry above, independently confirmed in a SECOND jitter consumer (probe placement,
not TAA reprojection) — the fix pattern (swap for the R2/Roberts-2018 additive-recurrence sequence,
titan_rendering_core::halton::r2_sequence) carries over directly. See RENDERING_BUG_HISTORY.md
2026-07-13 "Follow-up (round 4)" for the full isolation proof.
-
A large, static (not periodic) magnitude asymmetry between two probes/texels that should be exact
geometric mirrors, surviving every world-cache/clipmap-indexing exoneration, is a RESAMPLING
EFFECTIVE-SAMPLE-COUNT artifact, not a lookup-math defect (2026-07-13, round 5 of the Lattice
top-left-wall-reflection asymmetry). Before chasing floor()/voxel-index math further, check whether
the pass reduces N independently-traced samples down to M<N via a "trace M, reassign/duplicate the
rest" resampling scheme with NO importance-ratio reweight applied to the copied value — this
quadruples (or worse) the variance of the per-probe aggregate against perfectly ordinary, non-bug
per-direction geometric sensitivity (e.g. which of a fixed ray set clears a wall's finite edge),
because the "winning" subset is chosen by an importance ranking that itself carries the same
per-direction noise. Isolate by tapping the RAW per-texel atlas (not the tile mean) and checking
whether nonzero texels come in exact multiples of the fan-out ratio (here, blocks of 4 — a
48+r/3-style 3-low:1-high reassignment) rather than a smooth gradient; confirm by toggling the
reassignment off (if a debug override already exists for it) and checking the ratio collapses toward
1.0 at a frame-0 control (no reprojection/temporal contamination) before checking steady state (a
self-referential resampling-target feedback loop, once already confirmed elsewhere, can still amplify
the SAME root cause further at steady state without being a second, independent bug). See
RENDERING_BUG_HISTORY.md 2026-07-13 "Follow-up (round 5)" for the full CPU-side wc_lookup_cell
mirror-symmetry proof (floor-based voxel grids mirror as k ↔ n-1-k, not k ↔ -k — don't assume a
naive negation is the correct mirror check for a floor-indexed grid) and the frame-0-vs-steady-state
discriminator.
-
VIZ-decode hazard: some scalar taps store channels LINEARLY in the .ppm, not Reinhard+gamma;
applying the Rgba16Float Reinhard inversion to a linear channel gives a wrong reading (16× too
small → a false "near-receiver" distance). CORRECTION (2026-07-11, round 22): sdf_march_debug
is NOT an example of the linear case — round 2's own decode (this file, sdf_march_debug's
repro command) and round 22's independent re-derivation both confirm it IS Reinhard+gamma-encoded
(t=(s/255)^2.2; c=t/(1-t), then hit_distance_m=r*32.0, cascade_idx=round(g*4.0-1.0)). Confirm
the tap encoding per-tap before trusting a decode; do not assume a prior round's hazard note names
the right tap — cross-check against the shader's own textureStore write and a known-value sanity
check (e.g. a cascade index that should round to an integer).
-
A canon-conformance formula fix that REGRESSES a downstream artifact can mean the fix unmasked
a pre-existing, previously-INERT consumer-wiring divergence, not that the formula fix itself is
wrong. 2026-07-13, Lattice-Mid junction noise: correcting ShadingConfidence from a broken
formula (pinned ≈1.0 almost everywhere) to the canon formula (genuinely low/noisy at
poorly-sampled pixels) caused a 7.7× junction-noise regression — not because the corrected
formula was wrong, but because a SEPARATE consumer (the TAA confidence port,
blend = mix(1.0, blend, confidence)) had been wired to the WRONG pipeline stage's confidence
(raw pre-denoise ShadingConfidence instead of the canon-pinned post-Resolve/post-denoise
signal) the whole time — silently inert while the broken formula kept confidence near 1, and
newly load-bearing (and wrong) the moment the formula got fixed. Tell: the regressing
consumer already has its OWN comment citing the correct canon locus (here, "§4.1 step 15
Resolve") while its actual wiring reads from an earlier stage — a citation/implementation
mismatch is the signature, not a deliberate deviation. Isolate by finding every OTHER consumer of
the just-corrected value (grep the symbol, not just the pass that computes it) and testing each
with a defeat-toggle before assuming the newly-corrected formula itself needs re-tuning.
Falsifying/fixing test: neutralizing the SUSPECT CONSUMER alone (not touching the formula)
must both (a) collapse the regression and (b) the collapse must be reproducible by routing the
consumer to a genuinely different, still-varying signal from the stage canon actually pins — not
merely by defeating the consumer outright (a defeat-only "fix" re-pins confidence to a constant,
silently re-breaking the very formula correction that triggered the investigation).
-
A "fast-update"/hysteresis-relax signal built from POSITION delta (hit-velocity, motion vectors,
reprojection distance) reads a non-trivial baseline EVERYWHERE, not selectively near the actual fast-
changing condition, when the sampling origin itself has intentional per-frame jitter (probe placement
jitter, TAA jitter, stochastic ray origin offset) — freeze the jitter (a debug override pinning it to
a fixed value) and re-measure BEFORE concluding the position-delta signal reflects real motion.
2026-07-13, GI EMA-lag follow-up: LATTICE_SCREEN_PROBE_FAST_UPDATE's hit-velocity metric read
~0.10-0.43 at EVERY probe in a static-hold scene, not just near the orbiting light — freezing the R2
screen-probe placement jitter (TITAN_LATTICE_PROBE_PLACE_FREEZE_JITTER=1) collapsed it to bit-exact
0.0 at all sampled probes, including each probe's own GI-radiance PEAK frame. A position-delta
signal is structurally blind to "static geometry lit by a moving light" — the traced hit POINT
doesn't move when only the incident light moves, so hit-velocity-based fast-update can never engage
for exactly that scenario, regardless of tuning. The general lesson: before trusting ANY hit-position/
motion-vector-derived "something is changing fast" signal, confirm what it's actually differencing —
if the position itself carries deliberate jitter, the signal's noise floor is set by the JITTER
MAGNITUDE, not by the phenomenon you're trying to detect.
-
A "fix" that shrinks a decay-length/temporal-lag metric is not proven correct until a causally-
unjustified control (a blunt threshold tweak with no semantic tie to the hypothesis) is shown to NOT
achieve comparable improvement, AND a static/unaffected-probe noise-regression check is run.
2026-07-13, GI EMA-lag follow-up: adding a radiance-delta term to LatticeProbeTemporal's fast-update
reduced GI-decay amplification at 3/6 hotspot probes (e.g. 2.67×→1.67×) — but a control (radiance
term neutralized, hit-velocity thresholds blunt-lowered so PLACEMENT-JITTER NOISE ALONE now trivially
saturates fast-update≈1.0 everywhere) achieved COMPARABLE OR BETTER reduction at the same probes,
with no causal justification whatsoever. Any change that raises how often an EMA's "fast" blend factor
fires will mechanically shorten decay length, independent of whether the trigger is semantically
correct — decay-length improvement alone cannot discriminate a correct fix from an indiscriminate
"always relax more" hack. The needed second check: measure residual noise (e.g. std of the signal
minus its own short moving-average) at a genuinely static/unaffected probe — both the targeted fix
and the blunt control measurably regressed it here (+68%/+82% vs baseline), proving the "improvement"
at hotspots was bought at a real, uncounted cost elsewhere. Always pair a decay/lag metric win with an
unrelated-region noise check before calling a temporal-blend change a net positive.
Full manifest invariants (catalogue design rules, verified pass order, SVGF defaults):
KB docs/agent-kb/titan-rendering-3d-agent/sp-a-pass-manifest.md.