Attached cdb to the live 2013 retail client at the Holtburg doorway + read the decomp. The indoor flap is a STRUCTURAL divergence, settled by measurement (not inference): - Retail has ONE render path: DrawInside(viewer_cell) every frame. NO inside/outside branch (RenderNormalMode's outside branch is dead code; is_player_outside only gates sky/lighting). "Entering a building" is not a render event — only the camera sweep resolving a different viewer_cell. Same path before/after threshold -> no seam. - Retail's eye JITTERS ~36um at rest yet membership is stable -> robustness is STRUCTURAL: many small per-building floods (~7/frame, ~2 cells each, via terrain BSP -> DrawPortal -> ConstructView(CBldPortal)), not one giant knife-edge flood. - Our 3 divergences: (D1) invented inside/outside branch (GameWindow.cs:7498, clipRoot = viewerRoot ?? _outdoorNode :7396); (D2) synthetic _outdoorNode; (D3) one unified flood. DECISION (user-approved): Option A — rip out branch + outdoor node, root always at the real viewer_cell, one DrawInside, per-building rendering. Phased, conformance-tested, visual-gated. REFUTED by measurement (do not retry): bounded-propagation/churn (maxPop=1, 0/63k reciprocals empty); byte-stable eye (retail's jitters ~36um — rest-snapcd974b2failed + regressed, reverted9b1857a). Lands the canonical exhaustive handoff for a FRESH session (docs/research/2026-06-08-full-retail-render-port-OPTION-A-handoff.md), the CLAUDE.md READ-THIS-FIRST banner, and reusable cdb apparatus. No project code changed; working tree at the known-good baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
34 KiB
HANDOFF — Full Retail Render Port (Option A): one DrawInside(viewer_cell) path, no inside/outside branch
Date: 2026-06-08 (evening)
Branch: claude/thirsty-goldberg-51bb9b (HEAD 9b1857a)
Status: Design DECIDED (Option A). No implementation started. This is the canonical pickup
document for a FRESH session. Read it top-to-bottom before touching code.
Author's note to the next session: this is the payoff of a ~4-week saga + one long
measurement session. The information below was expensive to obtain (live cdb on the real
2013 retail client). Do not re-derive it; do not re-guess. Build from it.
0. TL;DR (read this, then read the rest)
The indoor "flap"/flicker is not a bug to be fixed with a point change. It is the symptom of a structural divergence from how retail renders. We confirmed this by attaching cdb to the live retail client and reading the decompilation. The findings are unambiguous:
- Retail has exactly ONE render path:
DrawInside(viewer_cell), every frame. There is no inside/outside render branch. The "outside" branch inRenderNormalModeis dead code (compiler-constant).is_player_outsideonly gates sky/weather/lighting, never the render path. - "Entering a building" is NOT a rendering event in retail. It is only the camera sweep
resolving a different
viewer_cell(an outdoorCLandCell→ an indoorCEnvCell). The render code never asks "am I inside?". Same path before and after the threshold → no seam → no flap. - Retail's eye JITTERS ~36 µm at rest (measured, live). Retail's membership is stable anyway. So retail's stability is structural (coarse per-building visibility robust to jitter), NOT from a stable eye. Chasing a byte-stable eye is the wrong target — retail itself doesn't have one.
- We diverged in three ways: (1) we invented an inside/outside branch + a synthetic
_outdoorNode; (2) we do ONE giant unified flood where retail does many small per-building floods; (3) our camera boom jitters ~36× more than retail's.
The decision (user-approved 2026-06-08): Option A — full retail structural port. Rip out the
branch and the outdoor node. Always root at the real viewer_cell. One DrawInside. Render
terrain + per-building interiors from within that path the way retail does. Phase it; conformance-
test each phase against the measured retail values in this doc; visual-gate.
Next session's first move: run superpowers:brainstorming is NOT needed (design is decided);
go to superpowers:writing-plans to turn §6 (the design) into a phased implementation plan, then
superpowers:executing-plans/subagent-driven-development. But FIRST close the open traces in §8.
1. The decision and its scope
Option A — Full retail structural port. In scope:
- Remove the inside/outside render branch. Today
GameWindow.cs:7498doesif (clipRoot is not null) { DrawInside } else { DrawPortal }, whereclipRoot = viewerRoot ?? _outdoorNode(GameWindow.cs:7396). Retail has no such branch. - Root always at the real
viewer_cell(the cell the camera-collision sweep resolves — an outdoorCLandCellor an indoorCEnvCell), never a synthetic outdoor node. - One
DrawInside(viewer_cell)per frame. Terrain + sky draw from within it when the flood "sees outside"; per-building interiors draw per-portal via the terrain BSP. - Per-building view construction (retail does ~7 small per-building floods/frame), replacing our single unified flood.
Explicitly NOT the goal: "make the eye byte-stable" (retail's isn't); "add hysteresis/dead-zone to the clip" (band-aid, forbidden); "bound the portal re-enqueue churn" (there is no churn — measured).
The user's words: "Yes lets do full retail! A!" and earlier "NO code of the project is frozen so all options on the table." Nothing is frozen. This is a render-orchestration rewrite, done retail-faithfully, in phases.
2. Why this took ~4 weeks (the pattern is the diagnosis)
Over ~4 weeks the "root cause" was declared, with apparatus, at least seven times: two-pipe split → root-at-player-cell → viewer-cell metastability → camera-boom drift → physics rest-jitter → portal-flood re-enqueue churn → render-position jitter. Every one was a real, measured perturbation. Every fix failed or moved the symptom. That pattern is the signature of a system-level problem attacked one stage at a time (systematic-debugging skill, Phase 4.5: "3+ fixes failed → question the architecture").
The fundamental issue. The flicker is a binary decision ("is this cell visible: yes/no") made at a grazing knife-edge (the doorway portal, near-zero-area sliver), fed by a long, coupled chain that amplifies:
physics body → render-position interpolation → camera boom → camera-collision sweep
→ viewer cell → render branch → portal flood → clip → VISIBLE / NOT VISIBLE
Measured amplification: physics body byte-stable → render position jitters µm → eye jitters ~1.3 mm → at the end the continuous wobble is forced into a yes/no at a knife-edge → cell pops in and out. It is a pencil balanced on its tip: it doesn't matter which draft of air tips it, there's always another. You cannot stabilize a pencil-on-tip by hunting individual air currents. Every "I found the jitter source!" fix closed one draft while the pencil stayed on its point.
Why retail has the same knife-edge but doesn't flicker: retail uses the exact same grazing clip (we ported it). Retail doesn't flicker because its structure is robust to the jitter — many small per-building visibility decisions, not one giant knife-edge flood. Retail did NOT remove the jitter (its eye jitters ~36 µm too); it made the decision robust to it. That is the thing we never did, because we kept patching the noise instead of the structure.
3. THE ORACLE — how retail actually renders (measured + decompiled GROUND TRUTH)
This is the irreplaceable part. It was obtained from the live retail client (cdb) + the named decomp. Cite it; do not re-derive it.
3.1 Retail render architecture: ONE path
SmartBox::RenderNormalMode (0x453aa0, decomp line 92635) always calls
DrawInside(viewer_cell). The "outside" branch (LScape::draw) is dead code — the branch
predicate is the Binary-Ninja artifact edi_2 = -((edi - edi)) = xor edi,edi; neg edi = always
0, so the inside branch is taken every frame. is_player_outside (0x451e80, line 90996) returns
nonzero for an outdoor land cell (low 16 bits of objcell_id in [1, 0xFF]) but is not called
from the render dispatch — only from GameSky::Draw, UI, and lighting. There is no
inside/outside render branch in retail.
3.2 Call graph (from the decomp-flow research agent, verified against addresses)
SmartBox::RenderNormalMode (0x453aa0, line 92635)
└─ ALWAYS: RenderDevice::vtable->DrawInside(viewer_cell)
→ RenderDeviceD3D::DrawInside (0x59f0d0, line 427843)
→ PView::DrawInside(indoor_pview, viewer_cell) (0x5a5860, line 433793)
→ CEnvCell::curr_view_push(viewer_cell)
→ PView::add_views(this, cell->num_stabs, cell->stab_list)
→ Render::copy_view(cell->portal_view[-1], null, 4)
→ PView::ConstructView(this, viewer_cell, 0xffff) [CEnvCell overload, 0x5a57b0]
→ PView::DrawCells(this, result) (0x5a4840, line 432709)
├─ if outside_view.view_count > 0: LScape::draw(lscape) [terrain + sky]
└─ for each cell in cell_draw_list: draw portals, env geometry, objects
PView::DrawCells → LScape::draw (0x506330, line 267912)
→ GameSky::Draw
→ for each land block: RenderDeviceD3D::DrawBlock (0x5a17c0, line 430027)
→ DrawLandCell (terrain) ; DrawSortCell → DrawBuilding (0x59f2a0, line 427938)
outdoor_pview->outdoor_portal_list = building->portals <<< KEY
→ terrain BSP walk reaches BSPPORTAL leaves (magic "PORT" 0x504f5254):
BSPPORTAL::portal_draw_portals_only (0x53d870, line 326881)
→ for i in num_portals: RenderDevice::vtable->DrawPortal(in_portals[i], frame, 1)
→ RenderDeviceD3D::DrawPortal (0x59f0e0, line 427852)
→ PView::DrawPortal(outdoor_pview, portalPoly, ...) (0x5a5ab0, line 433895)
CBldPortal* bp = outdoor_portal_list[portalPoly->portal_index]
PView::add_views(this, bp->num_stabs, bp->stab_list)
PView::ConstructView(this, bp, portal, ...) [CBldPortal overload, 0x5a59a0]
viewpoint side-test vs portal plane
PView::GetClip(...) ; CEnvCell::GetVisible(bp->other_cell_id)
Render::copy_view(...)
PView::ConstructView(this, other_cell, bp->other_portal_id) [recurse]
if result: PView::DrawCells(this, ...) [draw that building's interior]
SmartBox::update_viewer (0x453ce0, line 92761)
→ compute pivot from part_array + camera_manager->pivot_offset
→ choose start cell: indoor (objcell low16 >= 0x100) → AdjustPosition(pivot); outdoor → player->cell
→ CTransition: init_object(player, 0x5c) ; init_sphere(1, viewer_sphere, 1.0) ; init_path(cell)
→ find_valid_position:
success → set_viewer(sphere_path.curr_pos, 0) ; viewer_cell = sphere_path.curr_cell
else AdjustPosition(sought_eye) → set_viewer ; viewer_cell = that cell
else set_viewer(player->m_position, 1) ; viewer_cell = null
NO snap / NO quantize / NO dead-zone. (The eye jitters anyway — see §3.4.)
3.3 Verbatim decomp excerpts (the load-bearing ones)
(a) RenderNormalMode branch — the "always DrawInside" proof (lines 92635–92702):
this = RenderDevice::render_device->m_bOpenScene;
if (this != 0) {
int32_t edi_2 = -((edi - edi)); // == 0 ALWAYS (xor edi,edi; neg edi)
int32_t ebx_1 = (edi_2 != 0 || this_1->viewer_cell->seen_outside != 0) ? 1 : 0;
// ... FOV ...
if (edi_2 == 0) { // ALWAYS taken — the INSIDE path
if (ebx_1 != 0) { // viewer cell can see outside →
uint32_t eax_1 = Position::get_outside_cell_id(&this_1->viewer);
LScape::update_viewpoint(this_1->lscape, eax_1); // aim terrain viewpoint outside
}
Render::update_viewpoint(&this_1->viewer);
RenderDevice::render_device_2->vtable->DrawInside(rd2, this_1->viewer_cell);
} else { // DEAD CODE — edi_2 is constant 0
LScape::update_viewpoint(...); Render::update_viewpoint(...);
Render::set_default_view(); Render::useSunlightSet(1);
LScape::draw(this_1->lscape);
}
}
(b) PView::DrawInside (lines 433793–433823) — how the indoor flood is set up:
void PView::DrawInside(PView* this, CEnvCell* arg2) {
CEnvCell::curr_view_push(arg2);
PView::add_views(this, arg2->num_stabs, arg2->stab_list);
Render::copy_view(arg2->portal_view.data[arg2->num_view - 1], null, 4);
edx_2 = PView::ConstructView(this, arg2, 0xffff); // flood from viewer_cell
PView::DrawCells(this, edx_2);
PView::remove_views(this, arg2->num_stabs, arg2->stab_list);
}
(c) PView::ConstructView(CEnvCell*, 0xffff) (lines 433750–433789) — the flood loop:
void PView::ConstructView(PView* this, CEnvCell* arg2, uint16_t arg3) {
this->outside_view.view_count = 0;
PView::master_timestamp += 1;
this->cell_todo_num = 0;
this->cell_draw_num = 0;
PView::InitCell(this, arg2, arg3);
PView::InsCellTodoList(this, arg2, 0.0);
while (this->cell_todo_num > 0) {
CEnvCell* cell = cell_todo_list.data[this->cell_todo_num - 1]->cell;
if (cell == 0) return;
this->cell_todo_num -= 1;
cell_draw_list.data[this->cell_draw_num++] = cell; // <- membership append
cell->portal_view.data[cell->num_view - 1]->cell_view_done = 1;
if (PView::ClipPortals(this, cell, 0) != 0) // clip → enqueue neighbours
PView::AddViewToPortals(this, cell);
}
}
(d) Per-building portal loop — BSPPORTAL::portal_draw_portals_only (lines 326940–326953):
// Reached at each BSPPORTAL leaf during the terrain BSP walk (front-to-back vs viewer):
int32_t i = 0;
if (this_1->num_portals > 0) do {
int32_t edx_4 = this_1->in_portals[i]; // CPortalPoly*
RenderDevice::render_device->vtable->DrawPortal(/*portal*/edx_4, /*frame*/arg2, /*mode*/1);
i += 1;
} while (i < this_1->num_portals);
…and PView::DrawPortal (lines 433895–433933) looks up outdoor_portal_list[portalPoly->portal_index],
add_views, then ConstructView(CBldPortal*) → if non-empty, DrawCells that building's interior.
This is the ~7 cv-bld calls/frame we measured. Per-building, small, robust.
(e) update_viewer eye-set (lines 92761–92892) — NO stabilization: see the call graph §3.2.
The eye is the result of a per-frame CTransition::find_valid_position sweep from the pivot to the
sought eye. No snap / quantize / dead-zone. (The research agent inferred "stable because inputs
stable"; the LIVE trace contradicts that — the eye jitters ~36 µm — see §3.4. The agent did NOT trace
where viewer_sought_position is written; that is open trace #1 in §8.)
3.4 LIVE MEASUREMENTS (cdb on retail at the Holtburg cottage doorway, 2026-06-08)
Retail binary: C:\Turbine\Asheron's Call\acclient.exe, MATCHES our PDB
(refs/acclient.pdb, GUID 9e847e2f-777c-4bd9-886c-22256bb87f32). PID this session: 32360.
- Membership at rest is STABLE. Camera held still:
PView.cell_draw_numsettled to a long unbroken run of 2 (brief4only at startup).SmartBox.viewer_cellpointer = 1 distinct value across the whole capture (byte-stable cell). Retail does NOT flap at rest. - Retail does PER-BUILDING floods.
ConstructView(CBldPortal*)(0x5a59a0) fired ~7×/frame, eachcell_draw_num ≈ 2. TheCEnvCelloverload (0x5a57b0) fired far less. Retail does NOT do one unified flood. - Retail's EYE JITTERS ~36 µm at rest — the decisive measurement. Reading
SmartBox.viewer.frame.m_fOrigin(raw float bits) with the camera held still:
Decoded jitter: X ~15 µm, Y ~36 µm, Z ~8 µm.pub=(431a51ab, 41d1d3c4, 42c0a914) x≈154.32 y≈26.23 z≈96.33 (raw IEEE-754 hex) pub=(431a51ab, 41d1d3c1, 42c0a914) pub=(431a51ac, 41d1d3bf, 42c0a914) ← X flips 1 ULP pub=(431a51ab, 41d1d3cc, 42c0a915) ← Z flips 1 ULP pub=(431a51ac, 41d1d3b9, 42c0a913) ← Y spans ~19 ULPspub == sought(eye uncollided at the open door, so the jitter is the camera boom itself, not the collision sweep). Retail's eye is NOT byte-stable. - Compare to acdream (measured earlier this session via
[pv-input]at the same doorway): our eye jitters ~1.3 mm in Y (≈36× retail), ourRenderPositionshows 15 distinct values at rest, our membership oscillates (flood8↔3,6↔3, etc.). Our physics body (rawPlayer) IS byte-stable — the jitter enters in the camera chain, NOT physics.
3.5 Struct offsets + symbols (from flap-render-lookup.cdb / flap-pos-lookup.cdb)
acclient!SmartBox::update_viewer @ 0x453ce0
acclient!SmartBox::RenderNormalMode @ 0x453aa0
acclient!SmartBox::is_player_outside @ 0x451e80
acclient!PView::ConstructView(CEnvCell*, ushort) @ 0x5a57b0
acclient!PView::ConstructView(CBldPortal*, CPolygon*,int,int) @ 0x5a59a0
acclient!PView::DrawInside(CEnvCell*) @ 0x5a5860
acclient!RenderDeviceD3D::DrawInside @ 0x59f0d0
struct PView:
+0x000 outside_view : portal_view_type
+0x048 draw_landscape : Int4B
+0x04c outdoor_portal_list : CBldPortal** (set per-building by DrawBuilding)
+0x050 cell_draw_list : DArray<CEnvCell*>
+0x060 cell_draw_num : Uint4B (THE membership count)
+0x064 cell_todo_list : DArray<CellListType*>
+0x074 cell_todo_num : Uint4B
+0x078 lscape : LScape*
struct SmartBox:
+0x008 viewer : Position (the published eye)
+0x050 viewer_cell : CObjCell* (the cell the eye occupies)
+0x058 viewer_sought_position : Position (pre-sweep desired eye)
+0x0f8 player : CPhysicsObj*
struct Position: +0x004 objcell_id:Uint4B +0x008 frame:Frame
struct Frame: +0x000 qw,qx,qy,qz:Float +0x010 m_fl2gv[9]:Float +0x034 m_fOrigin:Vector3
⇒ SmartBox.viewer.objcell_id = +0x0c ; viewer origin x/y/z = +0x44 / +0x48 / +0x4c
⇒ SmartBox.viewer_sought_position.origin = +0x94 / +0x98 / +0x9c
4. Our divergences (precise, with file:line)
| # | Divergence | Where (acdream) | Retail truth |
|---|---|---|---|
| D1 | Inside/outside render branch | GameWindow.cs:7498 if (clipRoot is not null){DrawInside}else{DrawPortal}; root at GameWindow.cs:7396 clipRoot = viewerRoot ?? _outdoorNode |
No branch. Always DrawInside(viewer_cell). |
| D2 | Synthetic _outdoorNode (outdoor-as-cell) as root when eye outside |
GameWindow.cs:7396, OutdoorCellNode.cs, PortalVisibilityBuilder.Build if (cameraCell.IsOutdoorNode) (PortalVisibilityBuilder.cs:88) |
Root is the real outdoor CLandCell the eye occupies. |
| D3 | One unified flood (PortalVisibilityBuilder.Build from one root) |
RetailPViewRenderer.DrawInside → PortalVisibilityBuilder.Build (RetailPViewRenderer.cs:43); look-in via DrawPortal → BuildFromExterior (RetailPViewRenderer.cs:92) |
Many small per-building floods via terrain BSP → DrawPortal → ConstructView(CBldPortal). |
| D4 | MaxReprocessPerCell = 16 cap (re-enqueue band-aid) |
PortalVisibilityBuilder.cs:51 |
No cap; bounded structurally. (And: there is no re-enqueue churn — measured maxPop=1.) |
| D5 | EyeInsidePortalOpening guard (degenerate-portal hack) |
PortalVisibilityBuilder.cs (EyeInsidePortalOpening, ~235–244, 793–833) |
Retail's 3D clip needs no such special case. |
| D6 | Reciprocal clip on ProjectToNdc not ProjectToClip |
PortalVisibilityBuilder.ApplyReciprocalClip (~697–747) |
acdream split to dodge drift. |
| D7 | Render-position interpolation layer (ours, not retail) | PlayerMovementController.ComputeRenderPosition (PlayerMovementController.cs:810) Lerp(prev, curr, accumFrac) |
Retail renders at the authoritative position; the only nearby retail cite is the 30 Hz physics tick gate (CPhysicsObj::update_object :283950), NOT a render-interp. |
| D8 | Camera boom ~36× looser than retail | RetailChaseCamera (RetailChaseCamera.cs) damping + ApplyConvergenceSnap (SnapEpsilon 0.0004 m); collision sweep PhysicsCameraCollisionProbe.SweepEye |
Retail boom jitters ~36 µm; no snap in update_viewer. SECONDARY — fix the structure first. |
D1–D3 are the primary structural divergences Option A removes. D4–D8 are accumulated band-aids / secondary; most fall away once D1–D3 are done, but each must be removed deliberately (each was added to paper over a real problem — see §7 DO-NOT and the in-code comments).
5. The render pipeline as it exists today (so you know what you're rewriting)
- Entry:
GameWindow.csrender loop, ~7180–7800.RetailChaseCamera.UpdateproducesPosition(eye) +ViewerCellId.viewerRootresolved ~7209–7211;clipRoot = viewerRoot ?? _outdoorNode(7396). Branch at 7498:DrawInside(indoor/unified) vsDrawPortal(exterior look-in). RetailPViewRenderer(src/AcDream.App/Rendering/RetailPViewRenderer.cs):DrawInside(ctx)→PortalVisibilityBuilder.Build(rootCell, eye, lookup, viewProj)(line 43);DrawPortal(ctx)→PortalVisibilityBuilder.BuildFromExterior(candidateCells, …)(line 92). Post-flood:ClipFrameAssembler.Assemble, thenDrawLandscapeThroughOutsideView,DrawExitPortalMasks,DrawEnvCellShells,DrawCellObjectLists.PortalVisibilityBuilder(src/AcDream.App/Rendering/PortalVisibilityBuilder.cs): the flood. PortsConstructView/ClipPortals/AddViewToPortalsBUT as ONE flood with theMaxReprocessPerCellcap, theEyeInsidePortalOpeningguard, and the NDC reciprocal.OutsideViewis the terrain-through-door region.IsOutdoorNodespecial-cases the synthetic outdoor root.PortalProjection(PortalProjection.cs):ProjectToClip+ClipToRegion— faithful port of retailPView::GetClip(0x5a4320/:432344) +ACRender::polyClipFinish(:702749, the w=0 clip). KEEP THIS — it is correct; the problem is never the clip math, it's what feeds it.CellVisibility(CellVisibility.cs): cell membership /stab_list/seen_outside/ InsideSide side-test — faithful toCellManager::ChangePosition(0x4559B0) +grab_visible_cells(:311878).- Camera:
RetailChaseCamera.cs(boom,ApplyConvergenceSnapfromd2212cf),PhysicsCameraCollisionProbe.cs(SmartBox::update_viewersweep port),CameraController.cs(picks RetailChaseCamera vs legacyChaseCamera).
6. The design — Option A (phased; each phase conformance-tested + visual-gated)
The fresh session should run
superpowers:writing-plansto expand this into a task plan. The phases below are the architecture; the plan adds the bite-sized steps.
Guiding invariant (retail): every frame, root the render at the real cell the camera eye is
in (viewer_cell), and run ONE DrawInside. Outdoor terrain + per-building interiors are products
of that single path, not of a separate branch.
Phase R-A1 — Collapse to one root, one path (remove D1 + D2).
- Make
clipRoot= the real cell the camera-collision sweep resolved (RetailChaseCamera.ViewerCellId→ the actualLoadedCell, outdoorCLandCellor indoorCEnvCell). Delete the?? _outdoorNodefallback and theIsOutdoorNodespecial-case inPortalVisibilityBuilder. - Delete the
else { DrawPortal(...) }branch (GameWindow.cs:7613–7690). One call site:DrawInside(viewer_cell)every frame. - Requires: an outdoor
CLandCellmust be a validDrawInsideroot whose flood immediately "sees outside" (OutsideViewfull) so terrain draws. This is the retail behavior (viewer_cellis a land cell when outside). Open design point: acdream'sLoadedCellmodel may not currently represent the outdoor land cell as a floodable cell — see open trace #3 (§8). Resolve before coding. - Conformance: at the doorway, the frame before and after crossing the threshold run the same
code path;
[pv-input]outRootstops toggling (there is no outRoot concept anymore).
Phase R-A2 — Per-building floods (remove D3).
- Replace the single unified
Build(when looking at buildings from outside) with retail's per-building constructions: during the terrain/landblock draw, for each visible building portal, run a smallConstructViewrooted at that building portal (theCBldPortaloverload), flooding only that building's cells. PortBSPPORTAL::portal_draw_portals_only(0x53d870) →PView::DrawPortal(0x5a5ab0) →ConstructView(CBldPortal)(0x5a59a0). - This is the robustness mechanism (small coarse per-building visibility absorbs eye jitter).
- Conformance: capture
cell_draw_numper building ≈ 2 (matches retail §3.4); membership stable as the (jittering) eye moves within a cell.
Phase R-A3 — Remove the band-aids (D4, D5, D6) made dead by R-A1/R-A2.
- With per-building bounded floods,
MaxReprocessPerCell(D4),EyeInsidePortalOpening(D5), and the NDC reciprocal (D6) should be removable. Remove each deliberately, re-running the conformance + the existingPortalVisibilityBuilderTests. Do NOT removeProjectToClip/ClipToRegion(faithful).
Phase R-A4 (optional, secondary) — Tighten the camera boom toward retail (D8); reconsider the render-position interpolation (D7).
- Only if, after R-A1–R-A3, residual flicker remains AND it correlates with eye jitter > retail's ~36 µm. Match retail's boom damping/snap. Do NOT chase byte-stable (retail isn't). Treat the render-position interpolation as suspect but DO NOT rip it out blindly (it prevents 30 Hz judder; removing it regressed the branch last time — see §7).
Testing strategy (critical — this is how we stop shipping unverified changes):
- Conformance tests against the measured retail values in §3.4 (cell_draw_num per building ≈ 2, membership stable under eye jitter, one path across the threshold). These run WITHOUT the live client.
- Keep all existing
PortalVisibilityBuilderTestsgreen where still applicable. - Keep the 14
PlayerMovementControllerTestsgreen. - Visual gate is the acceptance test (the user at the doorway). But the conformance tests are the PRE-gate — never ship to the visual gate on a red/absent conformance test again.
- Re-attach cdb to retail to capture any NEW retail value the implementation needs (the workflow in §7 is proven and fast).
7. DO NOT RETRY (every one of these is evidence-disproven — re-trying wastes days)
- "Make the eye byte-stable at rest." Retail's eye jitters ~36 µm (§3.4, MEASURED). Byte-stable
is the wrong target. My render-position rest-snap fix this session did this, failed (no change)
AND regressed the inside/outside flap, and was reverted (
cd974b2→ revert9b1857a). The jitter source is also NOTRenderPosition(stabilizing it changed nothing) — it is downstream in the camera boom / sweep. Don't re-snap RenderPosition. - "Bound the portal re-enqueue churn" / bounded-propagation / enqueue-once. There is no churn:
measured
maxPop = 1across 13k oscillating frames; 0 of 63k reciprocals ever clipped empty (ACDREAM_PROBE_PORTAL_CHURN, this session). The whole bounded-propagation plan (docs/superpowers/plans/2026-06-08-portal-flood-bounded-propagation.md+ the2026-06-08-portal-flood-enqueue-once-port-design.mdspec) is REFUTED by measurement. The apparatus commits (687040b,e6fe4c6,a866c51) are fine to keep as probes; the fix premise is dead. - Physics rest µm-jitter (
d6aa526era). Refuted: 216k standstill records, 0 re-snaps; body byte-stable. 4 GREEN rest-stability tests prove it. - Camera-drift / viewer-cell metastability dead-zone (2026-06-05 3-part plan). The dead-zone is
±0.2 mm in
point_inside_cell_bsp; the eye crosses by metres, not sub-mm — irrelevant to this symptom. The boom snap (Part 1,d2212cf) is already shipped and KEPT. - Two-pipe inside/outside split — that was the ORIGINAL approach, abandoned 2026-05-30. Do not resurrect it. Retail has neither two pipes NOR a branch — it has ONE path (§3.1).
- Render-side debounce/grace/hysteresis on the branch or the clip — forbidden band-aid
(
feedback_no_workarounds). - Trusting a decomp INFERENCE about runtime behavior without a live trace. This session burned a fix on the inference "RenderPosition jitter → eye jitter." The cdb-on-retail workflow (§ below) is the antidote: MEASURE, don't infer.
8. OPEN TRACES — finish these BEFORE writing the implementation plan
The oracle is ~90% complete. Three things must be traced/decided first (each is a focused cdb capture and/or decomp read; the workflow below makes each ~10 min):
- Where
SmartBox::viewer_sought_positionis written (the camera boom that produces the ~36 µm-jittering sought eye). The decomp agent did NOT find the write site (it's in thecamera_manager/ spring-arm chain). Trace it (cdb bp on writes toSmartBox+0x58, or readCameraManagermethods) to know exactly how tight retail's boom is and what to match in D8. PView::ClipPortals(0x5a4...) andPView::AddViewToPortals(0x5a52d0:433446) — the per-cell flood propagation. Not yet read in detail. Needed for a faithful per-buildingConstructViewport (Phase R-A2). Read both.- How retail's
DrawInside/ConstructViewhandle aCLandCell(outdoor)viewer_cell— i.e. the pure-outdoor and outside-looking-in root. Confirm the outdoor land cell floods such thatoutside_viewis full and per-building portals render via the terrain BSP. AND decide how acdream'sLoadedCell/cell model represents the outdoor land cell as a floodable root (Phase R-A1 open design point). This is the single biggest unknown for the rewrite.
Also nice-to-have: viewer_sphere radius used in update_viewer (the agent didn't look it up; our
port uses 0.3 m — PhysicsCameraCollisionProbe.ViewerSphereRadius).
9. Apparatus (this session — REUSE IT, don't rebuild it)
Retail-debugger toolchain (PROVEN this session):
- Binary:
C:\Turbine\Asheron's Call\acclient.exe— verified pairs withrefs/acclient.pdb(py tools/pdb-extract/check_exe_pdb.py "C:/Turbine/Asheron's Call/acclient.exe"→ MATCH). - cdb:
C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe. - Attach + capture pattern (background, tee to a log):
& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" -p <PID> -cf <script>.cdb *>&1 | Tee-Object -FilePath <log> - Watchouts learned this session: (1) inside a bp action use
.detach, NOTqd(cdb ignoresqdin bp actions — perretail-viewer-cell.cdb); top-levelqdis fine. (2) The captures exit with code 5 but still produce the full log — exit 5 is expected here, not a failure. (3) The bps fire every render frame regardless of camera motion, so a counter-bounded capture auto-completes fast; cue the user to be doing the thing (sweeping) the whole window, or just capture the still pose. (4) Read floats as raw hex (%08xofpoi(addr)) to see byte-stability directly. (5)dt acclient!Type @ecx fieldreads a named field with no manual offset.
cdb scripts created (in tools/cdb/):
flap-render-lookup.cdb— symbol + PView/SmartBox type dump (the source of §3.5).flap-pos-lookup.cdb— Position/Frame type dump (eye-origin offsets).flap-render-capture.cdb— per-framecell_draw_num+viewer_cellptr (both ConstructView overloads). The membership-stability capture. (.logopenpath is hardcoded — edit per run.)flap-eye-stability.cdb— per-frame eye origin (raw hex) pub + sought. The ~36 µm finding.
Capture logs (worktree root, UNTRACKED — large; gitignore or delete after):
flap-render-retail.log(still pose),flap-render-retail-sweep.log,flap-eye-stability.log,flap-render-lookup.log,flap-pos-lookup.log, and the*-cdb-console.logfiles.
acdream-side probes (still in code, useful for the diff):
ACDREAM_PROBE_PVINPUT=1→[pv-input](eye/player/rawPlayer/yaw + flood count, 6dp) — the acdream eye/membership signal.launch-flap-verify.ps1runs it light.ACDREAM_PROBE_PORTAL_CHURN=1→[portal-churn](per-Build re-enqueue + reciprocal pre/post) — the apparatus that proved churn=0. Heavy (1.5 KB/line) — lags the client; use sparingly.ACDREAM_PROBE_FLAP=1→[flap]/[flap-cam]/[flap-sweep]/[pv-trace]— per-frame side-test + projection. Heavy.- All gated in
AcDream.Core.Rendering.RenderingDiagnostics(andPhysicsDiagnostics). - These are throwaway apparatus. Strip them once the port ships and is visual-gated.
Launch scripts: launch-flap-verify.ps1 (light pv-input), launch-flap-churn.ps1,
launch-flap-capture.ps1. acdream live-launch env vars: see CLAUDE.md "Running the client".
10. Git state & test baseline (start point for the fresh session)
- Branch
claude/thirsty-goldberg-51bb9b, HEAD9b1857a= revert of the failed rest-snap fix. Working tree is at the known-good baseline (the cutover-flip state: the huge inside↔outside flap is GONE; only the grazing-doorway flicker residual remains). - This session's commits (newest first):
9b1857a(revert) ←cd974b2(the bad rest-snap fix, reverted) ←b3a9884(launch-flap-churn.ps1) ←a866c51(churn anchor test) ←e6fe4c6(churn probe) ←687040b(churn flag) ←a3dadbf(bounded-propagation plan — now REFUTED) ←ab6ed90/6c3a96b/d6aa526/… (the refuted-diagnosis trail). - The churn probe (
687040b/e6fe4c6/a866c51) is fine to keep (inert when off; it's the apparatus that disproved the churn hypothesis). The bounded-propagation plan/spec are refuted — mark them superseded by this handoff. - Test baseline (post-revert, verified):
PlayerMovementControllerTests14/14 green;PortalVisibilityBuilderTests(App) green; build green. (Full-suite has documented static-leak flakiness — run targeted.) - The two NEW tests I added for the rest-snap were removed by the revert (correct — the fix was wrong).
11. How the fresh session should start (concrete)
- Read this whole doc, then §3 (the oracle) again. Read the two memory entries
project_indoor_flap_rootcause(will be updated to point here) andreference_render_pipeline_state. - Confirm the baseline:
git log --oneline -3(HEAD9b1857a);dotnet buildgreen; the targeted tests green. - Close the §8 open traces (viewer_sought_position write site; ClipPortals/AddViewToPortals;
the outdoor
CLandCellroot). Use the §9 cdb workflow on live retail if the user can run it; else decomp-read. Do not start the implementation plan until these three are answered — they determine the Phase R-A1 design (especially how to represent the outdoor land cell as a floodable root). superpowers:writing-plans→ expand §6 into a phased task plan (R-A1 → R-A4) with conformance tests against §3.4's measured values.- Implement phase-by-phase; conformance test PRE-gate; visual gate is acceptance; cdb-measure any new retail value you need. Never ship to the visual gate on an unverified change again.
- When it lands and is visual-verified: strip the apparatus (§9 probes), update the roadmap + milestones (M1.5), update memory, mark the flap CLOSED.
12. The one-sentence version (for when you're tired)
Retail draws inside and outside with one path rooted at whatever cell the eye is in, and it survives a jittering eye because its per-building visibility is coarse and robust — so stop patching the noise, delete our inside/outside branch and unified flood, and build the one retail path.