From 6aabe0b5ac27468778ab942dabb4c3fe8d13fd67 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 6 Jul 2026 16:19:38 +0200 Subject: [PATCH] diag #176/#180: isolation toggles pin the residual stripes on the camera sweep, not the render The #176 gate-2 failure ("stripes/triangles flickering when the camera is pushed into walls; nothing when zoomed out") is NOT a render defect. Isolation apparatus added this commit: - ACDREAM_LIGHT_DEBUG shader modes (mesh_modern.vert/frag + uLightDebug upload in EnvCellRenderer/WbDrawDispatcher): 1 = ambient-only, 2 = dynamics killed, 3 = raw vLit field (texture ignored). The pattern SURVIVES mode 3 -> not texture; lives above the light data. - ACDREAM_CLIP_DEBUG=1 (RenderingDiagnostics.ClipDebugNoShellTrim + the EnvCellRenderer slot-fill gate): shell pass draws cells WHOLE (retail's shape). Pattern survives -> the per-cell clip trim is exonerated. With every render suspect dead, an autonomous visual loop (synthetic back-into-wall input + GDI window captures + the [flap-sweep] probe) pinned the mechanism numerically: at a compressed moving boom the camera-collision sweep is BISTABLE - consecutive sweeps with ~1.4 mm input drift flip the first-contact solution 0.27 m along the boom (pulledIn 0.27<->0.53, every ~5-10 frames, all 368k sweeps ok=True), and at ~1700 fps unsynced every monitor refresh tear-interleaves the two views = the stripe/hatch patterns. Filed as #180 with the retail anchor: viewer_sought_position is STATEFUL (SmartBox 0x00452d75 feeds the CURRENT swept viewer into CameraManager::UpdateCamera 0x00456660 and assigns the return to the sought, 0x00452d84) - the target converges to the collided position instead of re-rolling the full knife-edge ray per frame like our RetailChaseCamera does. SweepEye itself ports update_viewer 0x00453ce0 faithfully and is exonerated. Also recorded in #176: the site-A weenie light-registration leak (a portal's I100 light stacked x2->x4 over one session as re-CreateObject re-registered it under fresh entity ids). The #176 lighting fix (d8984e87) remains live-verified; #176 re-gates after #180 lands. ISSUES: #180 filed, #176 updated. Suites: Core 2599+2skip; toggles inert by default. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 67 ++++++++++++++++++- .../Rendering/Shaders/mesh_modern.frag | 9 +++ .../Rendering/Shaders/mesh_modern.vert | 7 ++ .../Rendering/Wb/EnvCellRenderer.cs | 7 +- .../Rendering/Wb/WbDrawDispatcher.cs | 2 + .../Rendering/RenderingDiagnostics.cs | 30 +++++++++ 6 files changed, 120 insertions(+), 2 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index dc922e3b..9f442dc1 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,57 @@ Copy this block when adding a new issue: --- +## #180 — Camera-collision sweep bistable at a compressed boom → per-frame eye strobe (the #176 "stripes") + +**Status:** OPEN — mechanism PINNED numerically; fix = port retail's stateful sought-position +**Severity:** HIGH (the visible flicker/stripe artifact the #176 gate keeps failing on; corridor camera constantly rides walls) +**Filed:** 2026-07-06 +**Component:** camera / physics (NOT render — every render suspect eliminated by isolation) + +**Description (user, Facility Hub):** "geometrical patterns, triangles, that get +weird stripes… especially when I push the camera to the wall or the openings of +the corridor. If I zoom out and the camera does not touch the walls I get no +pattern." Reproduced autonomously (synthetic back-into-wall + GDI window +captures, `launch-176-cameye.log`). + +**Root cause (probe-pinned):** while the compressed chase boom moves along/near +a wall, the `SweepEye` first-contact solution is BISTABLE: consecutive +`[flap-sweep]` records show the sweep INPUT moving ~1.4 mm (the player's glide) +while the OUTPUT flips 0.27 m along the boom (`pulledIn` 0.27 ↔ 0.53, eye +(78.500,−38.633,−3.845) ↔ (78.669,−38.815,−3.938)), re-flipping every ~5–10 +frames — a knife-edge graze on the corridor's double-faced slabs (the #137 +threshold family, camera edition; all 368k sweeps returned ok=True — this is +NOT the fallback path). Each flip hard-cuts the whole view matrix; at ~1700 fps +with no vsync every monitor refresh tear-interleaves BOTH views → fine +stripe/hatch patterns over surfaces (worst near seams where the 0.27 m parallax +is largest), flickering with movement. Confirmed present in the pure light +field (`ACDREAM_LIGHT_DEBUG=3` still shows it → not texture) and with the +shell clip trim disabled (`ACDREAM_CLIP_DEBUG=1` → not the clip gate). +Explains the residual #176 flicker reports post-lighting-fix: one of the two +alternating eyes sees the purple under-room parallax, the other doesn't. + +**Retail anchor (the fix):** retail's sought position is STATEFUL — +`SmartBox` (0x00452d75) calls `CameraManager::UpdateCamera(mgr, &ret, +&this->viewer)` with the CURRENT swept viewer and assigns the RETURN to +`viewer_sought_position` (0x00452d84): the next frame's target derives from +the collided position (converges; re-extends gradually), so mm-jitter never +re-tests the full-length knife-edge ray per frame. acdream's +`RetailChaseCamera` recomputes the full-length desired boom from scratch every +frame. Fix = read `CameraManager::UpdateCamera` (0x00456660) + the sought +derivation and port the stateful shape — NO damping band-aid on the swept +result without that reading (workaround rule). + +**Files:** `src/AcDream.App/Rendering/RetailChaseCamera.cs` (boom/desired-eye), +`src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs` (`SweepEye` — port +verbatim per update_viewer 0x00453ce0, exonerated), `[flap-sweep]` probe +(ACDREAM_PROBE_FLAP) is the apparatus. + +**Acceptance:** camera pressed into walls/openings while moving → no +stripe/hatch patterns, no per-frame view jumps (consecutive-frame eye deltas +stay continuous in `[flap-sweep]`); camera glides along walls like retail. + +--- + ## #179 — Lightning flash has no indoor gate (dormant until weather strobes ship) **Status:** OPEN (dormant — zero production impact today) @@ -179,7 +230,21 @@ gate passes, then strip. **Gate:** stand in the `0x8A020164` corridor, turn back-and-forth across the seam — the purple wedge must hold steady (its faceted SHAPE is retail-correct and stays). -**Status:** 🟡 FIX SHIPPED — pending user gate +**⚠️ GATE 2026-07-06 (evening): FAILED on a RESIDUAL that turned out to be a +SEPARATE mechanism — flickering stripe/triangle patterns when the camera is +pushed into walls/openings. Isolation toggles killed every render suspect +(texture via `ACDREAM_LIGHT_DEBUG=3`, shell clip trim via `ACDREAM_CLIP_DEBUG=1`, +coplanar dat pairs, double-draws, seals); the artifact is the CAMERA-COLLISION +sweep strobing the eye 0.27 m per few frames at a compressed boom → tear- +interleaved views = stripes. Split to #180 (camera/physics). The lighting half +of #176 (the pool tracking the camera via flood scoping) remains FIXED and +live-verified; re-gate #176 after #180 lands. Also filed from this arc: the +site-A weenie light-registration LEAK (a portal's I100 light stacked ×2→×4 +over one session as CreateObject re-sends re-registered it under fresh +entity ids — `[seam-ent]` L= showed `01D4:I100` four times) — fold into the +A7 arc or fix with #180's gate.** + +**Status:** 🟡 lighting fix SHIPPED + verified; residual flicker = #180 (camera strobe) **Severity:** MEDIUM (visible artifact along every corridor seam in the M1.5 dungeon) **Filed:** 2026-07-06 **Component:** render — floor-portal polygons / portal surface state diff --git a/src/AcDream.App/Rendering/Shaders/mesh_modern.frag b/src/AcDream.App/Rendering/Shaders/mesh_modern.frag index 4f344369..e51cbcfc 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_modern.frag +++ b/src/AcDream.App/Rendering/Shaders/mesh_modern.frag @@ -16,6 +16,7 @@ in flat uint vTextureLayer; // alpha < 0.05 (skip empty fragments — large // transparent overdraw cost otherwise) uniform int uRenderPass; +uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here // SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1. struct Light { @@ -85,6 +86,14 @@ void main() { // Per-vertex Gouraud lighting from the vertex shader (ambient + capped lights). vec3 lit = vLit; + // #176 stripe-hunt mode 3: show the raw per-vertex light field (texture + // ignored). Stripes visible HERE = a vertex-lighting artifact; absent = + // the pattern comes from texture/per-pixel machinery. Throwaway diagnostic. + if (uLightDebug == 3) { + FragColor = vec4(min(lit, vec3(1.0)), 1.0); + return; + } + // Lightning flash — additive scene bump (matches mesh_instanced.frag). lit += uFogParams.z * vec3(0.6, 0.6, 0.75); diff --git a/src/AcDream.App/Rendering/Shaders/mesh_modern.vert b/src/AcDream.App/Rendering/Shaders/mesh_modern.vert index dd8f7f4a..23f5d817 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_modern.vert +++ b/src/AcDream.App/Rendering/Shaders/mesh_modern.vert @@ -132,6 +132,11 @@ uniform mat4 uViewProjection; // uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845. uniform int uDrawIDOffset; uniform int uLightingMode; // A7 Fix D: 0 = OBJECT (plain Lambert + sun), 1 = ENVCELL (half-Lambert wrap, no sun) +// #176 stripe-hunt isolation modes (ACDREAM_LIGHT_DEBUG, throwaway diagnostic): +// 0 = off; 1 = ambient-only vLit (all point/sun contributions killed); +// 2 = DYNAMIC point lights killed (purples + viewer fill off, statics stay); +// 3 = handled in the frag (raw vLit visualization, texture ignored). +uniform int uLightDebug; // SceneLighting UBO — binding=1 in the UBO namespace (GL keeps the SSBO and UBO // binding tables separate, so this coexists with the binding=1 BatchBuffer SSBO @@ -178,6 +183,7 @@ vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) { // bake's 1/d³ distance-cube, which makes a tight concentrated pool. No per-light // cap — D3D accumulates then saturates, which accumulateLights does via min(pointAcc,1). if (L.coneAngleEtc.y > 0.5) { + if (uLightDebug == 2) return vec3(0.0); // #176 stripe hunt: dynamics killed vec3 Ldir = toL / max(d, 1e-4); float ndl = max(0.0, dot(N, Ldir)); if (ndl <= 0.0) return vec3(0.0); @@ -214,6 +220,7 @@ vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) { vec3 accumulateLights(vec3 N, vec3 worldPos, int instanceIndex) { vec3 lit = uCellAmbient.xyz; + if (uLightDebug == 1) return lit; // #176 stripe hunt: ambient only // SUN / directional — OBJECT path only (mode 0). retail's EnvCell path // (minimize_envcell_lighting) enables only dynamic lights, NEVER the sun, so diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index 552a067d..e8d454a4 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -878,6 +878,8 @@ public sealed unsafe class EnvCellRenderer : IDisposable _shader.SetInt("uRenderPass", (int)renderPass); _shader.SetInt("uFilterByCell", 0); _shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun) + // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. + _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode); // Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when // moving"): upload uViewProjection HERE rather than inheriting it from @@ -1306,7 +1308,10 @@ public sealed unsafe class EnvCellRenderer : IDisposable // instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance. if (_clipSlotData.Length < uniqueInstanceCount) _clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)]; - if (_cellIdToSlot is null) + // #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell + // instance to slot 0 (no-clip) — retail draws cell shells WHOLE. + if (_cellIdToSlot is null + || AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim) { Array.Clear(_clipSlotData, 0, uniqueInstanceCount); } diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 4663f903..772ea9f2 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -910,6 +910,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable // A7 Fix D D-3/D-4: object path — plain Lambert points + sun. MUST set // explicitly (shared GL uniform; EnvCellRenderer sets it to 1). _shader.SetInt("uLightingMode", 0); + // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. + _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode); // #128 self-heal: fresh re-request dedup per Draw pass. _missRequested.Clear(); diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs index 7c6bc920..d4b97e7b 100644 --- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs +++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs @@ -336,6 +336,36 @@ public static class RenderingDiagnostics public static IReadOnlySet SeamDrawTargetCells { get; } = ParseSeamDrawTargets(Environment.GetEnvironmentVariable("ACDREAM_PROBE_SEAMDRAW")); + /// + /// #176 stripe-hunt shader isolation mode (ACDREAM_LIGHT_DEBUG) — + /// throwaway diagnostic, uploaded as uLightDebug by EnvCellRenderer + + /// WbDrawDispatcher each pass. 0 = off; 1 = ambient-only vertex lighting + /// (all point/sun contributions killed); 2 = DYNAMIC point lights killed + /// (the intensity-100 portal purples + the viewer fill off; statics stay); + /// 3 = raw vLit visualization in the fragment shader (texture ignored). + /// Discriminates lighting-driven stripes (gone at 1/2, visible in the field + /// at 3) from texture/per-pixel machinery (survive 1). Settable for a + /// future DebugPanel mirror. + /// + public static int LightDebugMode { get; set; } = + int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_LIGHT_DEBUG"), out var ldm) ? ldm : 0; + + /// + /// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1) — throwaway + /// diagnostic. When true, the EnvCell SHELL pass maps every instance to clip + /// slot 0 (the reserved no-clip region) instead of its cell's portal-slice + /// region — i.e. shells draw WHOLE, retail's shape (retail never clips cell + /// geometry; aperture exactness comes from the seal/punch depth writes + + /// far→near order, PView::DrawCells 0x005a4840). Discriminator for the + /// camera-against-wall stripe/hatch pattern: a knife-edge slice region (eye + /// on a portal plane / inside a wall) yields a clip plane near-parallel to + /// large surfaces → interpolated gl_ClipDistance ≈ 0 across them → per-pixel + /// sign dither = the hatch. Stripes gone with this on = the shell trim is + /// the mechanism. Entity/dispatcher clip routing is NOT affected. + /// + public static bool ClipDebugNoShellTrim { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_CLIP_DEBUG") == "1"; + /// Parse ACDREAM_PROBE_SEAMDRAW: "1"/"true"/empty → the default #176 /// Facility Hub set; otherwise a comma-separated hex cell-id list (same forgiving /// grammar as ). Internal for unit tests.