Round-2 integration: lag + flicker + GI-bake fixes land in main#83
Merged
Conversation
added 20 commits
July 4, 2026 09:55
…nd-2 audit) Profiler correctness (audit F9 — these skewed every round-1 measurement): - Rolling stats expire: a pass that stops running (feature toggled off) drops out of snapshot/summary/overlay after one window instead of showing a frozen average forever; entries evict after 4 windows. - Re-enabling the profiler starts a fresh session (clears rolling stats + frame histogram) instead of blending pre-disable numbers in. - One-shot warning when the 32-pair GPU timestamp budget is exhausted (was a silent truncation of later passes). - Restore the orphaned `post_fx` CPU bracket — begin() was lost at some point, leaving end() a no-op while a comment claimed the phase covered the composite tail's cost. - frame_end doc no longer claims the readback is non-blocking: it map_asyncs + poll(Wait)s every frame, serialising CPU<->GPU, so wall fps is pessimistic while profiling (measured ~3 fps at combat load). - Unit tests for the expiry + fresh-session semantics. Boot observability (audit F4 — the silent HW->SW GI fallback cost a debugger day): - windows: log adapter name/backend + ray_query/timestamp feature flags at device creation. - One-shot "bloom: ssgi trace backend = hw-ray-query|sdf-clipmap| hiz-screen" when the trace tier is chosen (re-logs on promotion). Crash-triage kit (audit F1, EN-020): - native/windows release profile carries line-tables-only debug info so `perry compile --debug-symbols` yields a usable PDB. - docs/crash-triage-windows.md: the standing WER LocalDumps + symbols + llvm-symbolizer workflow, and everything known about the layout- sensitive heap-read AV (3x reproduced pre-relink, gone after relink). - docs/tickets.md: EN-020 filed with repro history and ruled-out causes. Test repairs (pre-existing breakage from the round-1 merge): - material_system_tests: MaterialDrawCommand fixture gained the `model` field Tier 2 added — cargo test has not compiled since. - Regenerate the two golden references (lit_primitives_3d, many_point_lights_clustered_scene) that still encoded pre-round-1 shading; the other goldens pass unchanged and stay untouched.
…ma, sharpen knob - fs_main_scene's foliage backlit-transmission block was pasted TWICE (verbatim, 1.7x intended strength) and ran unshadowed — a canopy in another tree's shadow still glowed at full strength. Dormant in the shooter today (no MASK assets drawn) but a landmine for any cutout foliage. De-duplicated + multiplied by the sun shadow factor. (audit F3 side-finding) - 2D colors (shapes, textures, text) are now sRGB-decoded to linear before hitting the sRGB swapchain view — they were double-encoded: washed-bright HUD midtones, gamma-skewed glyph AA. Deliberately scoped to the 2D pass via a separate color_to_f32_srgb helper: the 3D immediate batch and model tints keep their historical raw /255 interpretation, because flipping those would silently re-tint every existing drawCube/drawModel call in shipped games. shapes_2d golden regenerated (midtones darken to their authored values); all 3D goldens unchanged. (audit F5) - New FFI `setSharpenStrength` / bloom_set_sharpen_strength: the composite unsharp mask was hardcoded 0.8 with no runtime control while visibly haloing silhouettes at 4K output. Default unchanged. (audit F8; manifest entry included — Perry silently no-ops undeclared natives) - docs/tickets.md: EN-021 (SSR+IBL exclusive ownership — deferred because a correct fix needs the env-miss fallback in the SSR pass, not just the ibl_spec complement) and EN-022 (motion vectors for material-system draws — the in-motion quality project).
…e leak
The translucent pass acquires TWO transients when a refractive material
is on screen (scene-colour + depth snapshots) but released only the
colour one. The pool has no auto-reclaim by design, so every frame:
- the still-in-use depth slot can never be reused,
- acquire() allocates a brand-new render-res Depth32Float (~8.3 MB at
1920x1080 — ~370 MB/s of driver allocation churn at 45 fps),
- the slot vector grows by one forever (linear scans slow down too).
This was the round-2 audit's "translucent_pass CPU mystery" (F7): the
bracket climbed 0.4 ms -> 6.7 ms over ~50 s in every session, tracking
RUN TIME, not scene load. Measured with the audit's instrumented combat
build, same workload, before vs after:
before (leaky): 750 us at t=8, dipping then climbing to 1144 us at
t=58 and still growing
after (fixed): 30-46 us, flat for the entire run (~16-26x)
On the 4 GB shared-memory dev iGPU the unbounded VRAM growth is also a
plausible contributor to long-session instability (EN-020 heap
pressure).
Also:
- transient pool: leak watchdog — warn at every power-of-two slot count
past 64, naming the acquire-without-release failure mode. This exact
bug cost a day of profiler archaeology; the next one costs a log line.
- docs/tickets.md: EN-023 (GI software path — colored bounce is
unreachable on non-RT adapters; measured achromatic <=2.5%).
Round-2 audit F10: compose is `hdr + ssr` and fs_main_scene already adds IBL specular into hdr, so SSR-hit pixels double-counted specular for roughness ~0.05-0.85 (worst on metals at r~0.55-0.75). The fix is ownership, not subtraction: - fs_main_scene scales ibl_spec by the complement of SSR's OWN roughness fade x its strength. The share rides the free dir_light_count.z lane (written per frame; 0 when SSR is disabled, so the full IBL term returns automatically). - The SSR march no longer returns black on a miss: camera-facing rays and off-screen/no-hit marches fall back to the env panorama sampled by the world-space reflection direction at the material path's roughness x 6 mip ramp, x env intensity (camera_pos.w), x the same fresnel/fade/strength weighting as a hit. Without this, reducing ibl_spec would darken every off-screen reflection - the reason the ticket deferred the one-line version. - SsrParams gains the view->world rotation (transpose of the view 3x3) + env LOD/intensity; SSR bind group gains the env panorama at bindings 9/10 and its cache invalidates wherever the env source swaps (HDR upload, panorama/procedural lighting-bg swaps). Suite green (99 lib + 7 golden; goldens unchanged within tolerance). Boot-smoked on the shooter at 4K - no WGSL errors, clean frames. Closes EN-021.
Round-2 audit F4: on non-RT adapters (the dev 760M) SSGI's SDF path
degraded every transformed instance to flat gray and the SW radiance
cache was light-source-only — colored bounce structurally impossible.
Fixed in this PR:
- instance data now carries WORLD-space AABBs alongside the object-
space ones (all four shader layout mirrors updated). The SDF broad-
phase compares its world-space clipmap hits against the world boxes
— the old object-space comparison only ever matched assets whose
vertices were already in world space (Sponza). HW paths keep using
the object-space boxes with hit.world_to_object, unchanged.
- broad-phase picks the SMALLEST containing box, not the first: the
shooter's +/-140 m terrain proxy otherwise swallowed every hit
(walls and trees included) and its mostly-empty side cards actively
darkened the bounce.
- the SW WSRC bake gains a ground-bounce term for below-horizon
octels (scene-average instance albedo x sun-shadowed irradiance +
half sky) — it previously returned ~black for every downward miss
ray, dropping the strongest real bounce source entirely.
Measured honesty (GI pose A/B, sky-normalized, settled):
- ship intensity 1.0: deltas within noise (<=1%), hue unchanged
- intensity 4.0: +1.4-2.1% luma in shaded receivers, hue still
unchanged (G/R 0.9456 vs 0.9460)
The data path is now correct, but visible colored bounce on the SW
tier remains capped downstream (probe resolve magnitude, composite AO
multiply on exactly the shaded receivers, radius/hit-rate at the
receivers). EN-023 stays open pointing at the resolve/integration
stage; decision D2-B (bank the ~1.6 ms on iGPU) remains sensible until
that lands.
Suite green (99 lib + 7 golden). Boot-smoked at 4K; A/B runs above
were on the live game.
Round-2 audit F6: the present mode was hardcoded Fifo at both surface config sites, which also made setTargetFPS inert — its sleep-based cap only engages when vsync is off, and vsync could never be off. - Renderer::set_present_mode(0=Fifo, 1=Mailbox, 2=Immediate): reconfigures the live surface on change; no-op when unchanged; logs the switch. All three modes are DXGI-native on Windows. - FFI bloom_set_present_mode + TS setPresentMode + manifest entry (Perry silently no-ops undeclared natives). Validated on the live game at 4K: "bloom: present mode = Immediate" followed by getFPS pinning at a steady 29 under setTargetFPS(30) — the first time the cap has ever engaged. This is the knob that makes audit decision D1 (operating point) testable: cap-45 / cap-60 / uncapped can now be compared like-for-like.
Round-2 audit F5: glyphs were rasterized at LOGICAL pixel size and the 2D projection stretched them x1.5 at 150% display scaling — every HUD string was a bilinear-magnified blur at 4K. - draw_text_ex rasterizes at size x dpi (dpi = physical surface width / logical width) and divides the layout metrics back to logical, so the projection's stretch lands the bitmap 1:1 on physical pixels. - measureText stays in logical units and stays exact: fontdue advances are strictly linear in pixel size, so advance(phys)/dpi == advance(logical) up to f32 rounding. - The glyph atlas warns once when full instead of silently wrapping UVs into garbage glyphs (grow/evict is the durable follow-up; the physical-size cache entries raise pressure, so the warning matters now). Validated on the live game at 4K: title + HUD render with correct layout and native-res rasters; no atlas warnings on the shooter's glyph set.
…erial draws The material ABI carried PerDraw.prev_mvp "for motion vectors" since day one, but every submit path filled it with the CURRENT mvp — world motion vectors were identically zero by construction, so TAA's motion-adaptive clamp never engaged for material-drawn geometry (round-2 audit F8, the primary TSR-shimmer mechanism). - MaterialSystem keeps a per-slot model history (draw slot = submission order, stable frame-to-frame — the same identity convention the cached-model path relies on). submit_draw / submit_draw_instanced now compose prev_vp x prev_model[slot] into PerDraw.prev_mvp; a fresh slot falls back to the current model (zero object motion on its first frame). - reset_draw_slot rotates the history and pins the previous frame's view-projection (from Renderer::prev_vp_matrix, which already fed PerView.prev_view_proj correctly). - The legacy prev_mvp parameter on the submit APIs is ignored and renamed _legacy_prev_mvp (callers passed current-mvp garbage). - material_abi.wgsl gains `abi_motion_vector(curr, prev)` — the core path's exact NDC-delta x 0.5 convention — plus doc guidance for static vs wind-displaced vertex shaders (PerFrame.delta_time already enables prev-time wind evaluation with no ABI change). Behavioral no-op until materials read prev_mvp / write velocity (the shooter's material updates ride a stacked shooter PR). Suite green: 99 lib + 7 golden, all unchanged.
# Conflicts: # docs/tickets.md
# Conflicts: # native/shared/src/ffi_core/visual.rs # native/shared/src/renderer/mod.rs # package.json # src/core/index.ts
Root cause of the round-1/round-2 'unreproducible' AVs and the title- screen freeze family: alloc_perry_string sized allocations exactly to header+payload, while Perry's compiled string scanners (split/indexOf) read word-at-a-time and touch up to a word past byte_len. When an allocation lands flush against an unmapped page the overread is an AV (reproduced 3/3 in ~7-29s with the shooter profiler overlay on: faults in perry_fn_...getProfilerOverlay at +0xe925 and ...getProfilerFrameHistory at +0xf0a3; historical EN-020 signature was main.exe+0xe8e5 reading 0x...FFF8). Fix: 16 zeroed pad bytes after every FFI string payload; capacity still reports byte_len so Perry never writes into the pad. Regression test included. Diagnostics added while hunting this (kept - they make the next silent death loud): SetUnhandledExceptionFilter that prints code + main.exe-relative address and writes a minidump via runtime-loaded dbghelp (import lib is not on perry's link line), eprintln on WM_CLOSE/WM_DESTROY (clean-exit path), one-shot log when surface acquire fails (headless-spin path). Report upstream to Perry: scanner overread on exact-sized string allocations.
…heir own slices Tail-padding engine-allocated strings (previous commit) did not stop the crashes: 3/3 runs still faulted at the same perry_fn offsets, proving the overread happens on Perry-INTERNAL allocations (split() slices fed to parseFloat), which the engine cannot pad. Route the per-frame profiler data across the FFI as f64s instead: bloom_profiler_row_count/_label/ _cpu_us/_gpu_us + _hist_* natives, getProfilerOverlay/getProfilerFrame- History rewritten on top (no split, no parseFloat; the label crosses whole and is only drawn). Packed-text FFIs stay for back-compat. Validated on the shooter: 3/3 runs x 90s gameplay with the overlay ON continuously, zero faults, correct overlay data - previously 6/6 dead within 7-29s of overlay time across two link layouts. EN-020 ticket updated with root cause + upstream repro; remaining: file against Perry, migrate the OBJ text loader off split/parseFloat when touched.
- Remove Surreal_Engine_Spec_v01.md (pre-rename draft; philosophy lives in README + the API itself) and bloom-renderer-spec.md (superseded; its Strategic Framing already survives as v2 spec section 6). bloom-renderer-spec-v2.md is now the only renderer spec and carries an as-built status block (wgpu/WGSL deferred MRT vs planned forward+/Slang; what landed from each track; where live status lives). - tickets.md: EN-019 Windows half field-validated on the 4K@150% dev box; EN-021/EN-022 implemented (PRs #78, #82 + shooter #4); EN-023 partially landed (PR #79), stays open pointed at resolve/AO. - crash-triage-windows.md: rewritten around the in-engine crash reporting (stderr + self-written minidumps), symbolizer flag drift, WinDbg/lldb tool notes, working repro patterns (PostMessage keys, pixel-diff freeze detection), EN-020 case study. - CLAUDE.md: Windows + test/golden build commands, consumer relink/perry-cache rules, real renderer/ tree, hard-won FFI rules (manifest discipline, EN-020 numeric-ABI rule, string_header, i64 scratch pattern), runtime/debug behavior, docs index.
…gation) One commit because the fixes interleave through renderer/mod.rs and intermediate splits would not build. Each fix is documented at its site. LAG (multi-second GPU stalls every ~5s of camera motion on iGPUs): - Scene-SDF-clipmap rebake was a single brute-force dispatch (64^3 voxels x ALL scene triangles, timestamp-less, refired on every 10m of eye travel -> 1-2.4s stall inside submit). Now: CPU triangle binning into 16^3 cells (one-cell expansion, conservative 2.5m narrow-band clamp, sphere-trace-safe) + 16 Z-layers/frame baked into a staging volume, copied over atomically on completion. Startup GI stall (~2.5s) gone too. - WSRC bakes one cascade per frame. Also fixes params aliasing: all same-frame cascade dispatches read the LAST cascade's uniform because queue.write_buffer applies before any encoded command executes. FLICKER (building face banding/popping, reported on round2/integration): - Material path sampled shadow cascades with the PREVIOUS frame's VPs (PerView uploaded before the shadow fit; the deferred path reads the fresh lighting buffer, which is why only material-path receivers banded). refresh_shadow_uniforms patches PerView after the fit; the splits .w mip-bias slot stays 0 as the material path always had it. - Cascade pancake extents now grow-fast/shrink-hysteresis at 2m steps; animated caster bounds crossing the old 1/16m ceil() steps toggled the fitted VP between two matrices every idle-anim cycle. - Auto-exposure: in-bin percentile interpolation (target was quantized to whole 0.22-log2 bins = 16% exposure steps), anchored 2% deadband (anchor persisted in the exposure texture .g, now Rg16Float), and gap-proportional adaptation rate. Measurement sits downstream of TAA, so its wiggle no longer round-trips into visible brightness hunting. - EN-022 motion vectors: prev_mvp / prev_view_proj were composed from the raw jittered previous VP, giving every static pixel a one-texel jitter-delta velocity that wobbled TAA history reprojection. All velocity references now use prev UNJITTERED proj+view with the CURRENT frame's jitter re-applied, cancelling exactly in (curr_ndc - prev_ndc). - GTAO temporal: the ao_delta>0.35 hard history refresh fired simultaneously across whole surfaces (per-frame 2-of-8 direction phase is globally shared), replacing converged AO with the noisiest possible single-frame estimate. Now: raw sample clamped to history +/-0.15 and the phase is dithered across each 2x2 quad so all 4 phases land every frame. TAA disocclusion reject deliberately NOT motion-gated -- it is what flushes chroma-poisoned history (luma-only variance clamp leaves chroma free); weakening it green-tints the frame within seconds. WINDOWS: - Windowed mode crashed on the first frame with a scene-reading translucent material: default render_scale is 0.5, so setRenderScale(0.5) no-ops and windowed (which never gets the WM_SIZE the borderless transition forces) kept construction-time render targets -> partial Depth32Float snapshot copy -> fatal validation error. Windows init now routes through resize() once. - Swapchain gets COPY_SRC so bloom_take_screenshot's readback is legal, and its failure paths now eprintln instead of being swallowed. (The FFI is still never invoked by Perry-compiled code -- upstream bug.) Measured on the 4K/Radeon 760M box: rotating-camera gameplay 40fps with 1-2.4s freezes -> locked 59-60fps, GI enabled throughout; startup worst frame 2.5s -> ~90ms; shadow banding eliminated (wall-region captures 15-16% alternation -> 0.0% with TAA off, stable with TAA on).
The YCoCg clamp was luma-only (per-channel RGB clamping had caused chromatic sparkle on grazing stone), but fully unclamped chroma let stale history colour bleed through on high-contrast edges: green terrain fringing crawled along cloud and canopy silhouettes during camera motion — user-visible as flickering. Clamping Co/Cg at 3x the luma gamma bounds gross cross-object bleed while staying far looser than the sparkle-prone hard clamp. Verified in windowed gameplay captures: cloud-edge fringing gone, no reintroduced sparkle.
Perry drops the game-side takeScreenshot FFI call entirely (PerryTS/perry#6087), so trigger the capture in the window procedure where no compiler sits in the path: F12 (initial press only) requests the swapchain readback and writes screenshot_<unix-ms>.png into the working directory. Depends on the earlier COPY_SRC surface-usage fix; verified end to end via PostMessage-injected F12 -> valid 4K PNG.
The composite unsharp mask ran at strength 0.8 (the engine already flagged it as visibly haloing silhouettes at 4K). Under half-res TSR the building silhouette edge wobbles a sub-pixel per frame, and a strong unsharp turns that wobble into a crawling bright/dark rim. Lower the default to 0.5 and clamp the detail term to +/-0.12: fine texture (small local contrast) sharpens as before, but extreme high-contrast edges no longer overshoot into a halo or amplify the sub-pixel shimmer.
|
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 (3)
📒 Files selected for processing (35)
📝 WalkthroughWalkthroughThis PR updates engine documentation and tickets, introduces a numeric profiler FFI ABI with Perry string padding hardening, reconstructs previous-frame model history for motion vectors, extends SSR with environment fallback and IBL specular ownership sharing, reworks scene SDF clipmap baking into an amortized job with world-space GI data, applies several renderer visual-stability fixes, and adds Windows crash-reporting/telemetry. ChangesBloom Engine renderer and tooling update
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant TS as TypeScript (index.ts)
participant FFI as Native FFI (game_loop.rs)
participant Profiler as Profiler (profiler.rs)
TS->>FFI: bloom_profiler_row_count/label/cpu_us/gpu_us(i)
FFI->>Profiler: snapshot()
Profiler-->>FFI: row data (f64 values)
FFI-->>TS: numeric row values
TS->>FFI: bloom_profiler_hist_count/cpu_us/gpu_us(i)
FFI->>Profiler: frame_history()
Profiler-->>FFI: history samples
FFI-->>TS: numeric history values
sequenceDiagram
participant Renderer as Renderer (mod.rs)
participant MaterialSystem as MaterialSystem
participant Shader as material_abi.wgsl
Renderer->>MaterialSystem: reset_draw_slot(prev_vp)
MaterialSystem->>MaterialSystem: rotate prev_models/cur_models
Renderer->>MaterialSystem: submit_draw(model, ...)
MaterialSystem->>MaterialSystem: compute prev_mvp = prev_vp * prev_model
MaterialSystem->>Shader: PerDrawUniforms(prev_mvp)
Shader->>Shader: abi_motion_vector(curr_clip, prev_clip)
sequenceDiagram
participant OS as Windows OS
participant WndProc as wndproc
participant CrashReport as crash_report module
participant DbgHelp as dbghelp.dll
OS->>WndProc: unhandled SEH exception
WndProc->>CrashReport: install()-registered filter invoked
CrashReport->>CrashReport: compute module-relative address
CrashReport->>DbgHelp: MiniDumpWriteDump
DbgHelp-->>CrashReport: minidump written to tools/.testout/dumps/
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
This was referenced Jul 8, 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.
Consolidates all round-2 work into main: every open round-2 PR (already merged into this integration branch) plus the 2026-07-06/07 fullscreen-lag + flicker + gameplay fixes that are not in any individual PR.
Round-2 PRs subsumed (all already merged here)
New fixes landed on top
Verified: rotating-camera gameplay 40fps+freezes -> locked 59-60fps; building gray-line flicker eliminated (native F12 diff).
Summary by CodeRabbit
New Features
Bug Fixes
Documentation