feat(render): indoor render WORKS — terminating portal flood + every-cell seal + look-in FPS
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the interior seals (live-verified by the user). Commits the session render-rewrite foundation together with the fixes that made it functional. - HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit); only its count is capped. CellViewDedupTests added. - Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey). - Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81 loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled). Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway, confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight): docs/research/2026-06-07-indoor-render-session-handoff.md. Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bff1955066
commit
1405dd8e90
27 changed files with 3635 additions and 814 deletions
|
|
@ -0,0 +1,135 @@
|
|||
# Retail PView Indoor Render Pseudocode (2013 EoR)
|
||||
|
||||
This note pins the indoor render port to the named retail decomp. The goal is
|
||||
behavioral fidelity: modern GL renderers may supply the draw calls, but the
|
||||
frame ownership, visibility graph, and draw order follow these functions.
|
||||
|
||||
## SmartBox::RenderNormalMode @ 0x00453aa0
|
||||
|
||||
```text
|
||||
if render device has open scene:
|
||||
outside = SmartBox::is_player_outside(player position)
|
||||
seenOutside = outside || viewer_cell.seen_outside
|
||||
set FOV/view distance
|
||||
|
||||
if !outside:
|
||||
if seenOutside:
|
||||
LScape::update_viewpoint(lscape, Position::get_outside_cell_id(viewer))
|
||||
Render::update_viewpoint(viewer)
|
||||
RenderDeviceD3D::DrawInside(viewer_cell)
|
||||
else:
|
||||
LScape::update_viewpoint(lscape, viewer.objcell_id)
|
||||
Render::update_viewpoint(viewer)
|
||||
Render::set_default_view()
|
||||
Render::useSunlightSet(1)
|
||||
LScape::draw(lscape)
|
||||
|
||||
FlushAlphaList()
|
||||
run targeting/render callbacks
|
||||
```
|
||||
|
||||
Important split: the top-level branch follows `is_player_outside`, while indoor
|
||||
render calls `DrawInside(viewer_cell)`.
|
||||
|
||||
## RenderDeviceD3D::DrawInside @ 0x0059f0d0
|
||||
|
||||
```text
|
||||
PView::DrawInside(RenderDeviceD3D::indoor_pview, viewer_cell)
|
||||
```
|
||||
|
||||
This is a thin forwarder. The PView owns the indoor frame.
|
||||
|
||||
## PView::DrawInside @ 0x005a5860
|
||||
|
||||
```text
|
||||
reset object scale
|
||||
CEnvCell::curr_view_push(root_cell)
|
||||
PView::add_views(root_cell.num_stabs, root_cell.stab_list)
|
||||
Frame::cache()
|
||||
Render::positionPush(root identity frame)
|
||||
Render::copy_view(root_cell.portal_view[last], null, 4) # full-screen root view
|
||||
forceClear = PView::ConstructView(root_cell, 0xffff)
|
||||
PView::DrawCells(forceClear)
|
||||
Render::framePop()
|
||||
PView::remove_views(root_cell.num_stabs, root_cell.stab_list)
|
||||
root_cell.num_view--
|
||||
```
|
||||
|
||||
## PView::ConstructView(CEnvCell*) @ 0x005a57b0
|
||||
|
||||
```text
|
||||
clear outside_view and cell draw/todo state
|
||||
insert root cell into distance-priority todo list
|
||||
|
||||
while todo is not empty:
|
||||
cell = pop nearest
|
||||
append cell to cell_draw_list
|
||||
InitCell(cell, otherPortalId)
|
||||
project/clip each portal against the current cell view
|
||||
exit portals append clipped polygons to outside_view
|
||||
interior portals append clipped polygons to neighbor portal_view
|
||||
newly discovered neighbors enter the todo list once
|
||||
|
||||
return forceClear flag
|
||||
```
|
||||
|
||||
`cell_draw_list` is the only indoor membership source. Later growth can add view
|
||||
polygons to a discovered cell, but does not create a second draw-list entry.
|
||||
|
||||
## PView::DrawCells @ 0x005a4840
|
||||
|
||||
```text
|
||||
if outside_view.view_count > 0:
|
||||
Render::useSunlightSet(1)
|
||||
Render::PortalList = this
|
||||
LScape::draw(lscape) # landscape clipped by outside_view
|
||||
D3DPolyRender::FlushAlphaList(0)
|
||||
render_device.frameStamp++
|
||||
if forceClear || portalsDrawnCount != 0:
|
||||
render_device.Clear(DepthOnly)
|
||||
|
||||
# Loop 1: exit portal masks, reverse cell_draw_list
|
||||
for cell in reverse(cell_draw_list):
|
||||
if cell.structure.drawing_bsp:
|
||||
push cell frame and surfaces
|
||||
for each current portal_view slice:
|
||||
CEnvCell::setup_view(cell, slice)
|
||||
for each exit portal:
|
||||
DrawPortalPolyInternal(portal polygon)
|
||||
pop frame
|
||||
|
||||
Render::useSunlightSet(0)
|
||||
Render::restore_all_lighting()
|
||||
|
||||
# Loop 2: closed cell shells, reverse cell_draw_list
|
||||
for cell in reverse(cell_draw_list):
|
||||
if cell.structure.drawing_bsp:
|
||||
push cell frame and surfaces
|
||||
for each current portal_view slice:
|
||||
CEnvCell::setup_view(cell, slice)
|
||||
DrawEnvCell(cell)
|
||||
pop frame
|
||||
|
||||
# Loop 3: cell object lists, reverse cell_draw_list
|
||||
for cell in reverse(cell_draw_list):
|
||||
Render::PortalList = cell.portal_view[last]
|
||||
DrawObjCellForDummies(cell)
|
||||
|
||||
restore object scale
|
||||
Render::useSunlightSet(1)
|
||||
```
|
||||
|
||||
There is no global indoor object, terrain, sky, weather, or particle pass. Every
|
||||
visible indoor object comes from the cell draw list, and the landscape appears
|
||||
only through `outside_view`.
|
||||
|
||||
## RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760
|
||||
|
||||
```text
|
||||
for object in cell.object_list:
|
||||
draw object under Render::PortalList
|
||||
attached effects/particles follow the owning object visibility
|
||||
```
|
||||
|
||||
acdream maps this to per-cell `WorldEntity.ParentCellId` buckets. Parentless
|
||||
live objects must not bypass the indoor PView graph.
|
||||
163
docs/research/2026-06-06-indoor-render-hang-rootcause.md
Normal file
163
docs/research/2026-06-06-indoor-render-hang-rootcause.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Indoor render HANG — root cause: `PortalVisibilityBuilder.Build` non-termination — 2026-06-06
|
||||
|
||||
> Report-only investigation (user chose "investigate more first"). **No code changed.**
|
||||
> Worktree `thirsty-goldberg-51bb9b`. This blocks the verbatim-DrawCells port's Task 2
|
||||
> visual gate: every indoor frame can freeze here.
|
||||
|
||||
## Symptom
|
||||
|
||||
Three launches of the client all **froze** (`AppHangB1`, Windows Event Log) within
|
||||
seconds-to-minutes of the camera being indoors at the Holtburg cottage. Not a crash —
|
||||
no access violation, no managed exception. The captured managed stack of the frozen
|
||||
render thread (`hang-stack.txt`, via `dotnet-stack`) shows it **CPU-spinning**:
|
||||
|
||||
```
|
||||
CPU_TIME
|
||||
CellView.Add(ViewPolygon)
|
||||
PortalVisibilityBuilder.AddRegion(CellView, List<ViewPolygon>)
|
||||
PortalVisibilityBuilder.Build(...)
|
||||
RetailPViewRenderer.DrawInside(...)
|
||||
GameWindow.OnRender(...)
|
||||
```
|
||||
|
||||
App.Tests 207/207 and Core 1331/4/1 are green; the bug is invisible to the suite (see §Evidence).
|
||||
|
||||
## Verdict
|
||||
|
||||
**It is NOT Task 2 (the verbatim-DrawCells / grey fix).** `Build(...)` runs at the very
|
||||
top of `DrawInside` ([RetailPViewRenderer.cs:43](../../src/AcDream.App/Rendering/RetailPViewRenderer.cs)),
|
||||
**before** any line Task 2 touched, and the call is byte-identical pre/post-change. Task 2's
|
||||
draw logic was independently confirmed correct in the run-1 log: `[render-sig] draw=[…]`
|
||||
equalled `ids=[…]` with `miss=[]`, and `[shell]` showed every visible cell drawing textured
|
||||
(`zh=0`). The grey fix works.
|
||||
|
||||
**Root cause:** `PortalVisibilityBuilder.Build`'s portal BFS does not terminate for real
|
||||
cottage geometry. It **re-enqueues a popped cell every time that cell's `CellView` grows**:
|
||||
`queued.Remove(cell.CellId)` on pop ([:122](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs))
|
||||
+ `if (grew && queued.Add(neighbourId))` on grow ([:289](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs)).
|
||||
Termination therefore depends entirely on growth stopping. Growth is gated only by
|
||||
`CellView.Add`'s **exact-match dedup** (`SamePolygon`, eps `1e-4`,
|
||||
[PortalView.cs:79](../../src/AcDream.App/Rendering/PortalView.cs)). The **near-side portal clip**
|
||||
(`ClipPortalAgainstView` → `PortalProjection.ProjectToClip` → `ClipToRegion`,
|
||||
[:474/:485](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs)) produces a polygon
|
||||
that is a hair different on each `A↔B` reciprocal round (float drift through the homogeneous
|
||||
project→clip round-trip with a non-identity cell transform). The dedup never matches the
|
||||
drifted near-duplicate → the region grows without bound → the cell re-enqueues forever →
|
||||
`CellView.Polygons` grows to N, and `CellView.Add`'s O(N) dedup scan makes the whole thing
|
||||
O(N²) → frozen.
|
||||
|
||||
## Evidence
|
||||
|
||||
1. **Captured stack** pins the spin to `CellView.Add ← AddRegion ← Build`, pure managed
|
||||
`CPU_TIME` (not a GL call, not blocked, not a fault).
|
||||
2. **The code already documents this exact failure** at
|
||||
[:694-697](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs): the *reciprocal*
|
||||
clip deliberately stays on the float-stable `ProjectToNdc` path *because*
|
||||
"per-round float drift defeated the CellView SamePolygon dedup, inflating a tight A<->B
|
||||
reciprocal view to ~4x its area." The **near-side** clip ([:474](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs))
|
||||
did not get the same treatment — it uses `ProjectToClip`.
|
||||
3. **The only bound was removed this session.** [:74](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs):
|
||||
"Fixpoint termination replacing the old `MaxReprocessPerCell` hard cap." The fixpoint never
|
||||
converges under drift; with the cap gone there is no other bound (no iteration cap, no
|
||||
max-polygon cap, no time bound).
|
||||
4. **It's the dirty-tree rewire the handoff said to KEEP.** `git diff --stat`:
|
||||
`PortalVisibilityBuilder.cs +426/−45` and `PortalProjection.cs +111` are **uncommitted**.
|
||||
`ProjectToClip` is part of the new `PortalProjection` lines. The handoff
|
||||
(`2026-06-06-verbatim-drawcells-port-pickup-handoff.md`) lists this rewire as the faithful
|
||||
foundation to preserve and says "the clip math is already faithful — do not harden the
|
||||
w-clip." The clip is faithful in the *picture* it computes; it is the *non-termination*
|
||||
that is broken.
|
||||
5. **Why the suite is green:** `PortalVisibilityBuilderTests` build cells with
|
||||
`WorldTransform = Matrix4x4.Identity` and axis-aligned quads in 2-cell **chains**
|
||||
(`cam → ground → exit`). No `A↔B` cycle, no transform-induced drift → the project→clip
|
||||
round-trip is exact → the dedup collapses duplicates → the BFS converges. The real cottage
|
||||
is a **cyclic** cell cluster (`0x016F–0x0175`, mutual portals) with **non-identity**
|
||||
transforms → drift + cycle → non-termination. The suite cannot reach the failing case.
|
||||
6. **Why run 1 survived 113 frames then froze:** `Build` converges at most camera poses; only
|
||||
specific poses create the non-converging drift cycle. The freeze coincided with the
|
||||
metastable doorway flip (`[render-sig] stable` went 39→0, visible-cell count 5→4) one frame
|
||||
before the log ended.
|
||||
|
||||
## Hypotheses (ranked)
|
||||
|
||||
1. **(confirmed)** Non-terminating BFS: re-enqueue-on-grow + `ProjectToClip` drift defeats the
|
||||
`SamePolygon` dedup → unbounded `CellView` growth. Falsify: a re-process cap, a
|
||||
drift-tolerant dedup, or `ProjectToNdc` on the near-side clip all make `Build` terminate.
|
||||
2. *(ruled out)* GPU/driver hang from a malformed draw — the stack is pure managed `CPU_TIME`
|
||||
in `CellView.Add`, never a GL call; no fault.
|
||||
3. *(ruled out)* Probe-output stdout saturation — disproven: the probe-free run also hung.
|
||||
4. *(ruled out)* Task 2 — `Build` is upstream of every Task 2 line and unchanged by it.
|
||||
|
||||
## Fix options (all additive — none reverts the dirty tree)
|
||||
|
||||
| | Fix | Touches | Pro | Con |
|
||||
|---|---|---|---|---|
|
||||
| **A** *(rec.)* | **Drift-tolerant dedup**: round clipped polygon vertices to a small grid (≈`1e-3`) before `AddRegion`, or widen/snaps `SamePolygon`'s match, so near-duplicates collapse → growth converges. | `CellView`/`AddRegion` | Fixes the actual root cause ("drift defeats dedup"); keeps the faithful `ProjectToClip`; preserves growth-propagation. ~10 lines. | Tolerance is a tuning constant (pick conservatively; over-merge = minor over-tighten). |
|
||||
| **B** | **Restore a re-process bound** (`MaxReprocessPerCell`-style cap on the BFS). | `Build` loop | Smallest; guarantees termination; doesn't touch clip. | A guard, not a root fix; may under-include a late-growing view. The user's "no workarounds" rule applies — this is the band-aid. |
|
||||
| **C** | **Near-side clip on `ProjectToNdc`** (what the reciprocal clip already uses). | `ClipPortalAgainstView` | Removes the drift source directly; consistent with `:694`. | Steps on this session's homogeneous near-eye clip work; the handoff's "don't harden the w-clip" is closest to here. |
|
||||
|
||||
**Recommended next step:** approve **A** (drift-tolerant dedup) — it closes the precise
|
||||
mechanism the code half-acknowledges at `:694`, terminates structurally, and leaves the
|
||||
faithful clip path intact. Implement in a follow-up (not report-only) session, then re-run the
|
||||
Task 2 visual gate (probe-free) at the cottage + cellar.
|
||||
|
||||
## What this is NOT
|
||||
|
||||
- **NOT** Task 2 / the grey fix — that is verified working (`draw==ids`, `miss=[]`, textured shells).
|
||||
- **NOT** a wrong-pixels / unfaithful-projection bug — it's a **termination** bug. The handoff's
|
||||
"the clip math is faithful, don't harden the w-clip" is about projection *correctness*; this is
|
||||
BFS *convergence*. Don't chase the w-clip.
|
||||
- **NOT** a GPU/shader/driver hang and **NOT** the probe firehose (both ruled out by the stack
|
||||
and the probe-free repro).
|
||||
|
||||
---
|
||||
|
||||
## Reassessment — is the dirty-tree builder rewire sound? (post Option A)
|
||||
|
||||
Option A (drift-tolerant `CellView.Add` dedup, `CellViewDedupTests` green) was implemented and the
|
||||
client relaunched. Result: the hang **moved out of `CellView.Add`** (A worked for its target) but
|
||||
**relocated to `ScreenPolygonClip.ClipByEdge`** via `ApplyReciprocalClip` (second captured stack,
|
||||
`hang-stack2.txt`). `ScreenPolygonClip.Intersect`/`ClipByEdge` are **both bounded `for` loops** —
|
||||
they cannot spin on one call — so the spin is the **outer `Build` BFS** still not terminating and
|
||||
calling them a runaway number of times. **Option A is necessary but not sufficient.**
|
||||
|
||||
### Git evidence (what the dirty rewire changed re: termination)
|
||||
|
||||
- **HEAD (committed)** near-side portal clip = `PortalProjection.ProjectToNdc` (float-stable;
|
||||
`git show HEAD:` line 146). **The dirty rewire switched it to `ProjectToClip`** (`ClipPortalAgainstView`,
|
||||
dirty line 474) — the homogeneous near-eye clip, introduced to fix the near/grazing-doorway flap/void.
|
||||
- The `MaxReprocessPerCell` **hard cap was removed earlier** (committed Phase U.2a `d880775`), replaced
|
||||
by "fixpoint termination." **Neither HEAD nor the dirty tree has a hard iteration bound.**
|
||||
- The dirty rewire's own comment (`PortalVisibilityBuilder.cs:519-522`) documents that
|
||||
`ProjectToClip` "produced per-round float drift that defeated the CellView SamePolygon dedup" — and
|
||||
applied that lesson **only to the reciprocal clip** (kept on `ProjectToNdc`), leaving the **near-side**
|
||||
clip on the drift-prone `ProjectToClip`.
|
||||
|
||||
### Soundness verdict
|
||||
|
||||
The builder's termination model is **unsound by construction.** It relies on the clipped regions
|
||||
reaching a geometric fixpoint — re-clipping a cell's view reproduces *exactly-equal* polygons that the
|
||||
dedup recognises — with **no hard iteration bound.** That only holds if the clip is float-stable.
|
||||
`ProjectToClip` (needed for faithful near-doorway projection) injects per-round drift, so re-clipping
|
||||
never reproduces an exactly-equal polygon, the dedup never catches it, and the re-enqueue-on-grow flood
|
||||
never converges → infinite loop. **You cannot have BOTH faithful near-doorway projection (`ProjectToClip`)
|
||||
AND convergence-via-exact-dedup-without-a-bound.** HEAD got away with it because `ProjectToNdc` was
|
||||
stable enough to converge (and it sealed — user-verified); the dirty switch tipped it into non-termination.
|
||||
The rewire fixed the *projection* and, apparently never having been launched, shipped a hang.
|
||||
|
||||
A's drift-tolerant dedup *narrows* the gap but cannot *close* it: for some geometry the per-round drift
|
||||
exceeds any fixed snap grid, so growth still produces new keys forever. Only a **hard bound** guarantees
|
||||
termination.
|
||||
|
||||
### Paths (for the user to choose)
|
||||
|
||||
| | Path | Termination | Projection fidelity | Risk |
|
||||
|---|---|---|---|---|
|
||||
| **1** *(rec.)* | Keep `ProjectToClip` + add **enqueue-once** bound (D) — the builder's own comment already calls enqueue-once "the hard termination guarantee"; the re-enqueue-on-grow is the bug. Keep A. | Guaranteed (≤N pops) | Full (faithful doorway clip kept) | Minor under-inclusion of late growth → visual-verify; widen to a cap if needed |
|
||||
| **2** | Keep `ProjectToClip` + add a **re-process cap** (B, restore `MaxReprocessPerCell`). Keep A. | Guaranteed (≤N×K) | Full | Less faithful than enqueue-once; a tuning constant |
|
||||
| **3** | **Revert** the near-side `ProjectToClip → ProjectToNdc` (back to HEAD). | Restored (HEAD converged) | **Loses** the rewire's near-doorway fix → reintroduces the flap/void (separate bug) | Throws away this session's projection work; contradicts the keep-the-dirty-tree directive |
|
||||
|
||||
A bound (paths 1/2) is the sound fix: it makes termination independent of clip drift, so the faithful
|
||||
`ProjectToClip` projection AND guaranteed termination coexist. **Recommendation: path 1** (enqueue-once +
|
||||
keep A), visual-verify for under-inclusion. Reverting (path 3) only trades the hang back for the
|
||||
flap/void.
|
||||
|
|
@ -0,0 +1,589 @@
|
|||
# Handoff - M1.5 Indoor Render / Retail PView Replacement Attempts - 2026-06-06
|
||||
|
||||
This is a **stop-and-handoff** note for the next agent. It records what was tried, what changed on disk, what the user still sees, and what evidence should drive the next step.
|
||||
|
||||
The user explicitly stopped this thread after repeated visual regressions. Do **not** continue the same patching loop. Treat all current uncommitted render work as suspect until re-audited against named retail.
|
||||
|
||||
## Worktree And Rules
|
||||
|
||||
- Worktree: `C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b`
|
||||
- Branch: `claude/thirsty-goldberg-51bb9b`
|
||||
- Starting HEAD called out by the user: `8116d10`
|
||||
- Do **not** branch or create a new worktree.
|
||||
- Do **not** push without asking.
|
||||
- Never run `git stash` or `git gc`.
|
||||
- PowerShell on Windows.
|
||||
- Launch logs are UTF-16.
|
||||
- Build before launching.
|
||||
- Use `apply_patch` for manual edits.
|
||||
- Do not revert dirty changes unless the user explicitly asks.
|
||||
|
||||
Current child handoff thread created before this file:
|
||||
|
||||
- Child thread id: `019e9d5c-bb34-7fe3-85cc-6b9065b4e882`
|
||||
- It was forked same-directory, not a new worktree.
|
||||
- A follow-up prompt was sent there with the immediate evidence and constraints.
|
||||
|
||||
## User-Visible State At Stop
|
||||
|
||||
The latest user report, after the most recent relaunch:
|
||||
|
||||
- Transition flaps still happen between outdoor/indoor, room/room, and cellar.
|
||||
- Ground floor became transparent instead of sealed.
|
||||
- Cellar remains broken.
|
||||
- Prior screenshots showed grey or black background filling cell openings.
|
||||
- Prior screenshots showed indoor walls losing texture or drawing as clear/background color.
|
||||
- Prior screenshots showed character cut in half on the cellar stairs.
|
||||
- User explicitly says we are back to old bugs and nothing feels solid.
|
||||
|
||||
Important: **do not claim any current code is fixed**. Build/tests passed for some pieces, but visual acceptance failed.
|
||||
|
||||
## Current Dirty State
|
||||
|
||||
`git status --short --branch` showed these tracked files modified:
|
||||
|
||||
- `src/AcDream.App/Rendering/ClipFrameAssembler.cs`
|
||||
- `src/AcDream.App/Rendering/ClipPlaneSet.cs`
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs`
|
||||
- `src/AcDream.App/Rendering/InteriorEntityPartition.cs`
|
||||
- `src/AcDream.App/Rendering/InteriorRenderer.cs`
|
||||
- `src/AcDream.App/Rendering/ParticleRenderer.cs`
|
||||
- `src/AcDream.App/Rendering/PortalView.cs`
|
||||
- `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs`
|
||||
- `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`
|
||||
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`
|
||||
- `src/AcDream.Core/Rendering/RenderingDiagnostics.cs`
|
||||
- `src/AcDream.Core/World/WorldEntity.cs`
|
||||
- `tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs`
|
||||
- `tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs`
|
||||
- `tests/AcDream.App.Tests/Rendering/InteriorEntityPartitionTests.cs`
|
||||
- `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs`
|
||||
- `tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs`
|
||||
- `tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs`
|
||||
- `tools/TextureDump/Program.cs`
|
||||
|
||||
Important untracked files include:
|
||||
|
||||
- `src/AcDream.App/Rendering/RetailPViewRenderer.cs`
|
||||
- `docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`
|
||||
- many probe logs and local scripts/images, including `launch-flap-shell-capture-relaunch.log`, `launch-pview-watermark-probe.log`, `a8-current-room-cellar-audit.txt`, `texture-current-room-surfaces.txt`, `analyze_*.py`, `retail-*-trace.log`, and several screenshots.
|
||||
|
||||
Diff size before this handoff file:
|
||||
|
||||
- 19 tracked files changed.
|
||||
- About 1593 insertions and 773 deletions.
|
||||
|
||||
## Validation That Passed But Did Not Prove Visual Correctness
|
||||
|
||||
After the last attempted PortalVisibilityBuilder patch:
|
||||
|
||||
```powershell
|
||||
dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~PortalVisibilityBuilderTests|FullyQualifiedName~PortalProjectionTests"
|
||||
```
|
||||
|
||||
Passed: 29/29.
|
||||
|
||||
```powershell
|
||||
dotnet build -c Debug --no-restore
|
||||
```
|
||||
|
||||
Succeeded, with 9 known warnings.
|
||||
|
||||
```powershell
|
||||
dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --no-build
|
||||
```
|
||||
|
||||
Passed: 196/196.
|
||||
|
||||
These results only prove the pure/tested slices compile and pass. They did **not** solve the live render.
|
||||
|
||||
## Retail PView Reference Already Written
|
||||
|
||||
New pseudocode note exists:
|
||||
|
||||
- `docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`
|
||||
|
||||
It summarizes:
|
||||
|
||||
- `SmartBox::RenderNormalMode @ 0x00453aa0`
|
||||
- `RenderDeviceD3D::DrawInside @ 0x0059f0d0`
|
||||
- `PView::DrawInside @ 0x005a5860`
|
||||
- `PView::ConstructView @ 0x005a57b0`
|
||||
- `PView::DrawCells @ 0x005a4840`
|
||||
- `RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760`
|
||||
|
||||
Core retail model from that note:
|
||||
|
||||
- Outdoor: `LScape::draw`, then portal/interior peering through PView portal paths.
|
||||
- Indoor: `DrawInside(viewer_cell)`.
|
||||
- `PView::ConstructView` builds `cell_draw_list`, per-cell `portal_view`, and `outside_view`.
|
||||
- `PView::DrawCells` draws outside landscape through `outside_view`, then reverse `cell_draw_list` exit masks, reverse shells, reverse object lists.
|
||||
- No global indoor terrain/entity/particle pass should bypass PView membership.
|
||||
|
||||
## Retail Functions That Still Matter
|
||||
|
||||
Re-read named retail before more code:
|
||||
|
||||
- `PView::AddViewToPortals @ 0x005a52d0`
|
||||
- `PView::ConstructView @ 0x005a57b0`
|
||||
- `PView::ClipPortals @ 0x005a5520`
|
||||
- `PView::FixCellList @ 0x005a5250`
|
||||
- `PView::AdjustCellView @ 0x005a5770`
|
||||
- `PView::OtherPortalClip @ 0x005a5400`
|
||||
- `PView::GetClip` around `0x005a4320`
|
||||
- `SmartBox::RenderNormalMode @ 0x00453aa0`
|
||||
- `SmartBox::update_viewer @ 0x00453ce0`
|
||||
- `RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760`
|
||||
|
||||
The critical retail detail not faithfully settled yet:
|
||||
|
||||
- Retail tracks `view_count` and `update_count`.
|
||||
- When a cell view grows after the cell was already processed, retail calls `FixCellList` / `AdjustCellView`.
|
||||
- Current acdream code only approximates this. It may not match draw-list ordering or downstream propagation.
|
||||
|
||||
## What We Tried
|
||||
|
||||
### 1. Treated symptoms as separate render leaks
|
||||
|
||||
The session started with symptoms that looked separate:
|
||||
|
||||
- dynamic objects and particles visible through ground when looking out from inside;
|
||||
- outside ground texture covering cellar entrance when looking in;
|
||||
- grey flaps when crossing cell boundaries;
|
||||
- missing cellar floor / grey cellar;
|
||||
- transparent or textureless interior walls.
|
||||
|
||||
The user correctly pushed back that these are probably one render-pipeline failure: indoor/outdoor, cells, shells, terrain, objects, particles, and doors must all agree on one visible-cell graph.
|
||||
|
||||
### 2. Gated dynamic objects and particles by ownership
|
||||
|
||||
Attempt:
|
||||
|
||||
- `WorldEntity.ParentCellId` was populated for player/spawns/teleports/motion updates.
|
||||
- `InteriorEntityPartition` was changed so live dynamic entities with an indoor `ParentCellId` go into their cell bucket instead of a global live-dynamic overlay.
|
||||
- `WbDrawDispatcher.ResolveEntitySlot` was changed so `ServerGuid != 0` no longer always means "draw unclipped indoors".
|
||||
- Particles were moved toward PView-scoped / owner-scoped behavior instead of a global indoor scene pass.
|
||||
|
||||
Effect:
|
||||
|
||||
- User reported this stopped much of the obvious dynamic-object/particle bleeding when looking out.
|
||||
- It did **not** fix grey/background transition flaps.
|
||||
- It did **not** fix cellar/floor/walls.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This direction is probably correct, but the exact routing must be audited. A later attempt also cleared clip routing to avoid character/shell cutting, so "PView membership" and "GPU clip slot routing" are currently mixed/confused.
|
||||
|
||||
### 3. Added/used a `RetailPViewRenderer`
|
||||
|
||||
Attempt:
|
||||
|
||||
- Added `src/AcDream.App/Rendering/RetailPViewRenderer.cs`.
|
||||
- Moved part of indoor draw orchestration into `RetailPViewRenderer.DrawInside`.
|
||||
- Added `DrawPortal` for outdoor-looking-in through `PortalVisibilityBuilder.BuildFromExterior`.
|
||||
- The renderer currently does:
|
||||
- `PortalVisibilityBuilder.Build`
|
||||
- `ClipFrameAssembler.Assemble`
|
||||
- `_envCells.PrepareRenderBatches(filter: drawableCells)`
|
||||
- `InteriorEntityPartition.Partition`
|
||||
- landscape through outside slices
|
||||
- exit masks
|
||||
- EnvCell shells
|
||||
- object buckets
|
||||
|
||||
Effect:
|
||||
|
||||
- This is not a full retail replacement yet.
|
||||
- User repeatedly saw unchanged or worse symptoms.
|
||||
- FPS was reported drastically down after one iteration.
|
||||
- Subsequent attempts produced missing textures / white or grey wall panels.
|
||||
|
||||
Current risk:
|
||||
|
||||
- `RetailPViewRenderer` is not truly verbatim retail. It keeps modern infrastructure and approximates PView with GPU clip slots and callbacks.
|
||||
- The user asked "have you ported retail verbatim?" and the honest answer remains no.
|
||||
- `GameWindow` still has a lot of orchestration, diagnostics, and render routing around this. It is not a small caller yet.
|
||||
|
||||
### 4. Reworked `ClipFrameAssembler` from one clip per cell to per-slice clip slots
|
||||
|
||||
Attempt:
|
||||
|
||||
- `ClipFrameAssembler` was rewritten toward per-polygon/slice output:
|
||||
- `CellIdToViewSlices`
|
||||
- `OutsideViewSlices`
|
||||
- per-slice `ClipViewSlice`
|
||||
- `TerrainClipMode` for outside-view landscape
|
||||
- The goal was to represent retail `portal_view` slices more closely.
|
||||
|
||||
Effect:
|
||||
|
||||
- The code is plausible as a draw assist, but it is not retail membership.
|
||||
- User saw regressions including black covers during transitions.
|
||||
|
||||
Current risk:
|
||||
|
||||
- The next agent must ensure `ClipFrameAssembler` never decides PView membership.
|
||||
- It should be draw-assist only.
|
||||
- Several failures looked like GPU clip slots cutting shells or characters at door/stair boundaries.
|
||||
|
||||
### 5. Disabled clip routing for shells/entities to stop character/stair cutting
|
||||
|
||||
Attempt:
|
||||
|
||||
- `RetailPViewRenderer.UseIndoorMembershipOnlyRouting` clears `_envCells.SetClipRouting(null)` and `_entities.ClearClipRouting()`.
|
||||
- Comment says retail portal views decide eligibility, but feeding those 2D views into GL clip distances slices characters and shells at stair/door boundaries.
|
||||
|
||||
Effect:
|
||||
|
||||
- This was a reaction to user screenshots where the character was cut in half on stairs.
|
||||
- It may reduce character slicing.
|
||||
- It may also mean shells/objects are currently only membership-gated, not portal-view clipped.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This is not a settled retail copy. It is an emergency compromise.
|
||||
- Retail does use per-view setup (`CEnvCell::setup_view`) around shell/object drawing. We need to know whether our GL clip-plane model is simply the wrong mechanism for that setup.
|
||||
|
||||
### 6. Tried EnvCell / DAT polygon side handling changes
|
||||
|
||||
Attempt:
|
||||
|
||||
- `ObjectMeshManager` changed CellStruct polygon side handling:
|
||||
- DAT `CullMode` interpreted as retail `CPolygon::sides_type`.
|
||||
- `0 = pos`
|
||||
- `1 = pos twice with reversed winding`
|
||||
- `2 = pos + neg surface`
|
||||
- `NoPos` / `NoNeg` still suppress faces.
|
||||
- Added explicit normal inversion / winding reversal logic.
|
||||
|
||||
Effect:
|
||||
|
||||
- User saw missing textures/white/grey interior panels after some launches.
|
||||
- The attempt did not fix the cellar or transition flaps.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This may be correct retail interpretation or may be partially wrong.
|
||||
- Audit with DAT dumps and retail/ACME references before keeping.
|
||||
- `a8-current-room-cellar-audit.txt` and `texture-current-room-surfaces.txt` may contain useful surface/cell evidence.
|
||||
|
||||
### 7. Tried outside-looking-in via `BuildFromExterior`
|
||||
|
||||
Attempt:
|
||||
|
||||
- `PortalVisibilityBuilder.BuildFromExterior` seeds interior cell views through outside-facing exit portals.
|
||||
- `RetailPViewRenderer.DrawPortal` calls it from outdoor branch.
|
||||
- Tests were added:
|
||||
- seeds interior cell through outside portal;
|
||||
- does not seed when camera is on interior side;
|
||||
- traverses deeper interior portals;
|
||||
- max seed distance skips distant exit portal.
|
||||
|
||||
Effect:
|
||||
|
||||
- User initially reported walls became visible looking in from outside, but ground/cellar entrance composition stayed wrong.
|
||||
- Later launches regressed to transparent/grey panels and missing textures.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This is probably needed, but the exterior portal path is not proven retail-faithful.
|
||||
- `BuildFromExterior` may now have duplicated-looking test diff context; inspect file carefully.
|
||||
|
||||
### 8. Tried broad "no hybrid" render routing in `GameWindow`
|
||||
|
||||
Attempt:
|
||||
|
||||
- `GameWindow` was changed so indoor path should call `RetailPViewRenderer.DrawInside`.
|
||||
- Outdoor path should draw world and call `DrawPortal`.
|
||||
- Global indoor terrain/entity/particle passes were reduced or bypassed.
|
||||
- New render signature diagnostics log:
|
||||
- `branch`
|
||||
- `root`
|
||||
- `viewerRoot`
|
||||
- `playerRoot`
|
||||
- `viewerCell`
|
||||
- `playerCell`
|
||||
- `gate`
|
||||
- `terrain`
|
||||
- `skyGate`
|
||||
- `zclear`
|
||||
- `sceneParticles`
|
||||
- `outSlices`
|
||||
- `outPolys`
|
||||
- `ids`
|
||||
- `draw`
|
||||
- object partition counts
|
||||
|
||||
Effect:
|
||||
|
||||
- User explicitly asked whether the hybrid was totally gone.
|
||||
- It is not safe to answer "yes" without auditing `GameWindow`.
|
||||
- Symptoms persisted, so either the routing is still hybrid or the PView graph/draw setup is wrong enough that "no hybrid" alone does not solve it.
|
||||
|
||||
Current risk:
|
||||
|
||||
- `GameWindow.cs` has a very large diff, around 1000 lines touched.
|
||||
- Next agent should not blindly keep it.
|
||||
- Audit all remaining global passes while `clipRoot != null`.
|
||||
|
||||
### 9. Tried PView `update_count`-style reprocessing
|
||||
|
||||
Attempt in the last aborted step:
|
||||
|
||||
- `PortalView.CellView.Add` now returns `bool` and deduplicates near-identical polygons.
|
||||
- `PortalVisibilityBuilder.Build` replaced `seen` with:
|
||||
- `queued`
|
||||
- `drawListed`
|
||||
- `processedViewCounts`
|
||||
- A cell can be requeued when its view grows.
|
||||
- Each processing pass clips portals against only newly added view polygons.
|
||||
- Similar logic was added to `BuildFromExterior`.
|
||||
- Added tests:
|
||||
- `Build_CollapsedInteriorPortalNearEyeBeyondHalfMeter_FloodsNeighbour`
|
||||
- `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit`
|
||||
|
||||
Effect:
|
||||
|
||||
- Focused tests passed.
|
||||
- Live probe after patch still showed `outPolys` toggling near root `0xA9B40172`.
|
||||
- User then reported transition flaps still there and now ground floor transparent.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This patch is **unaccepted** and may be wrong.
|
||||
- It approximates retail `update_count`, but does not necessarily implement `FixCellList`, `AdjustCellPlace`, or retail draw-list ordering correctly.
|
||||
|
||||
### 10. Widened eye-standing-in-portal fallback
|
||||
|
||||
Attempt:
|
||||
|
||||
- `EyeStandingPerpDist` widened from `0.5f` to `1.75f`.
|
||||
- Motivation: live cellar capture had `0174 -> 0175` traversable with `D=-1.41` but `ProjectToNdc` returned zero vertices.
|
||||
- The fallback still requires the perpendicular projection to land inside the portal opening.
|
||||
|
||||
Effect:
|
||||
|
||||
- It made one unit test pass for the cellar-style collapsed portal.
|
||||
- It did not solve live transitions.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This may be a bandaid, not retail.
|
||||
- It should be validated against `OtherPortalClip` / `GetClip` in named retail before keeping.
|
||||
|
||||
### 11. Tried reciprocal clip fallback for eye-in-opening
|
||||
|
||||
Attempt:
|
||||
|
||||
- Before `ApplyReciprocalClip`, code clones `clippedRegion` when `eyeInsideOpening`.
|
||||
- If reciprocal clipping empties the region, it restores the pre-reciprocal region.
|
||||
|
||||
Effect:
|
||||
|
||||
- Tests passed.
|
||||
- Live visual did not.
|
||||
|
||||
Current risk:
|
||||
|
||||
- This may over-include.
|
||||
- It is not proven retail-faithful.
|
||||
|
||||
## Critical Evidence From Logs
|
||||
|
||||
### Cellar startup: root 0174 only sees itself
|
||||
|
||||
From `launch-flap-shell-capture-relaunch.log`:
|
||||
|
||||
```text
|
||||
[flap] root=0xA9B40174 eye=(154.50,4.99,92.25) localEye=(7.43,2.51,-1.77) |
|
||||
p0->0x0175 D=-1.41 TRV proj=0 clip=-1 || outPolys=0 vis=1
|
||||
|
||||
[flap-cam] root=0xA9B40174 viewerCell=0xA9B40174 playerCell=0xA9B40174
|
||||
... terrain=Skip outVisible=False
|
||||
|
||||
[render-sig] frame=49 branch=RetailPViewInside root=0xA9B40174
|
||||
... terrain=Skip/skip sky=n zclear=n sceneParticles=none
|
||||
outSlices=0 outPolys=0 ids=[0xA9B40174] draw=[0xA9B40174]
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- The player/viewer/root are in cellar cell `0174`.
|
||||
- The only visible portal to stair connector `0175` is traversable.
|
||||
- Projection produces zero vertices.
|
||||
- The PView flood stops at the cellar.
|
||||
- Only the cellar draws; stair/main-floor cells are not in the visible set.
|
||||
|
||||
This is a direct candidate cause for missing floor/grey composition.
|
||||
|
||||
### Root 0172: outside view toggles on/off
|
||||
|
||||
From `launch-pview-watermark-probe.log`:
|
||||
|
||||
```text
|
||||
frame=3625 root=0xA9B40172
|
||||
p0->0x0173 D=... TRV proj=4 clip=4
|
||||
p1->0x016F D=5.28 TRV proj=0 clip=-1
|
||||
outPolys=1 vis=6
|
||||
ids include 0xA9B40170
|
||||
terrain=Skip/draw sky=Y zclear=Y
|
||||
|
||||
frame=3626 root=0xA9B40172
|
||||
p0->0x0173 D=... TRV proj=5 clip=5
|
||||
p1->0x016F D=5.39 TRV proj=0 clip=-1
|
||||
outPolys=0 vis=5
|
||||
ids missing 0xA9B40170
|
||||
terrain=Skip/skip sky=n zclear=n
|
||||
|
||||
frame=3647 root=0xA9B40172
|
||||
outPolys=1 ids include 0xA9B40170 terrain draw
|
||||
|
||||
frame=3648/3649 root=0xA9B40172
|
||||
outPolys=0 ids missing 0xA9B40170 terrain skip
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- The same root cell can alternate between seeing outside and not seeing outside.
|
||||
- `0x016F` is involved in the root flap line but projects to zero.
|
||||
- Sometimes `0x0170` becomes reachable and outside terrain/sky/depth clear run; sometimes it disappears.
|
||||
- The visible-cell list and outside-view list are not stable.
|
||||
|
||||
Open question:
|
||||
|
||||
- Is `0x016F` an outdoor/land cell, an env cell lookup miss, or a portal that retail handles differently?
|
||||
- Is the toggling caused by projection/clip degeneracy, wrong portal reciprocal handling, update-count propagation, or camera/viewer-cell root?
|
||||
|
||||
### Earlier known evidence: root 0171 vs player 0174 contradiction
|
||||
|
||||
From the older `2026-06-05-shell-sealing-cellar-floor-handoff.md`:
|
||||
|
||||
```text
|
||||
[flap-cam] root=0xA9B40171 viewerCell=0xA9B40171 playerCell=0xA9B40174
|
||||
...
|
||||
[flap] root=0xA9B40171 ... p1->0173 proj=0 ...
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- Earlier, camera/root was the room while player was cellar.
|
||||
- The flood did not seal the player's cell.
|
||||
- Later, after branch/viewer changes, there are also frames where root/player/viewer are all `0174` but the flood still fails on `0174 -> 0175`.
|
||||
|
||||
This means the problem is probably not only "wrong root"; it also includes projection/portal traversal/flood propagation or mesh-shell handling.
|
||||
|
||||
## What Not To Retry Blindly
|
||||
|
||||
Do not simply:
|
||||
|
||||
- switch the root to player cell as a workaround;
|
||||
- widen `EyeStandingPerpDist` further;
|
||||
- globally draw all indoor shells;
|
||||
- globally draw terrain/entities/particles while inside;
|
||||
- turn off all clipping and hope depth sorts it;
|
||||
- keep adding `if cellar` or Holtburg-cottage-specific handling;
|
||||
- claim "no hybrid" without auditing all `GameWindow` indoor/outdoor passes;
|
||||
- equate unit-test pass with visual correctness.
|
||||
|
||||
The user has explicitly asked for retail smoothness, not a new patch stack.
|
||||
|
||||
## Likely Root Problem Space
|
||||
|
||||
The next fix probably lives in one of these, but evidence must decide:
|
||||
|
||||
1. **PView graph construction is not retail-faithful.**
|
||||
- Missing or wrong `update_count` / `FixCellList` / `AdjustCellView`.
|
||||
- Wrong draw-list ordering when a processed cell receives new views.
|
||||
- Downstream portal propagation incomplete.
|
||||
|
||||
2. **Portal projection/clip behavior differs from retail.**
|
||||
- `0174 -> 0175` traversable but `proj=0`.
|
||||
- `0172 -> 016F` traversable but `proj=0`.
|
||||
- `OtherPortalClip` / `GetClip` may not match retail.
|
||||
|
||||
3. **Outdoor/exit portal classification is wrong.**
|
||||
- `OtherCellId=0xFFFF` is treated as exit/outside, but `0x016F` may be another kind of outside/land portal or missing env cell.
|
||||
- OutsideView may be created through a downstream path that acdream sometimes drops.
|
||||
|
||||
4. **Renderer draw setup is still hybrid or ordered wrong.**
|
||||
- `GameWindow` may still draw or skip global passes inconsistently.
|
||||
- Sky/terrain/depth clear decisions are visibly flapping with `outside_view`.
|
||||
|
||||
5. **EnvCell shell mesh/surface handling is wrong.**
|
||||
- Missing/transparent/white walls and floors may be mesh/surface/cull-side regressions.
|
||||
- Audit `ObjectMeshManager` side handling against retail and DAT dumps.
|
||||
|
||||
6. **GPU clip slots are being used as membership or hard clipping when retail uses view setup differently.**
|
||||
- Character cut in half on stairs strongly suggests hard clip-plane use on avatars/shells is wrong or applied at wrong pass.
|
||||
|
||||
## Suggested Next Procedure
|
||||
|
||||
1. Stop patching. Inspect the dirty diff first.
|
||||
2. Read `docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`.
|
||||
3. Re-read named retail functions listed above.
|
||||
4. Parse `launch-flap-shell-capture-relaunch.log` and `launch-pview-watermark-probe.log` around the cited frames.
|
||||
5. Add better probes if needed:
|
||||
- cell id;
|
||||
- portal index;
|
||||
- other cell id;
|
||||
- portal flags;
|
||||
- other portal id;
|
||||
- traversable decision;
|
||||
- standing distance;
|
||||
- projection vertex count;
|
||||
- clip vertex count;
|
||||
- reciprocal clip result;
|
||||
- outside-view add/skip reason;
|
||||
- cell view count / processed count / update count;
|
||||
- queue/requeue reason;
|
||||
- draw-list insertion/reorder.
|
||||
6. Decide from evidence whether `0174 -> 0175` and `0172 -> 016F` fail because of projection, reciprocal clip, cell lookup/classification, or update propagation.
|
||||
7. Patch only the retail mismatch.
|
||||
8. Build/test before launch.
|
||||
9. Launch with probes once.
|
||||
10. Then launch clean for FPS/visual feel.
|
||||
11. Do not call it done until the user visually confirms retail smoothness.
|
||||
|
||||
## Launch Command
|
||||
|
||||
Use PowerShell:
|
||||
|
||||
```powershell
|
||||
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||
$env:ACDREAM_LIVE = "1"
|
||||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||
$env:ACDREAM_TEST_PORT = "9000"
|
||||
$env:ACDREAM_TEST_USER = "testaccount"
|
||||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||
$env:ACDREAM_PROBE_FLAP = "1"
|
||||
$env:ACDREAM_PROBE_SHELL = "1"
|
||||
$env:ACDREAM_PROBE_VIS = "1"
|
||||
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||
Tee-Object -FilePath "launch-next-pview.log"
|
||||
```
|
||||
|
||||
For clean visual/FPS run, remove the probe env vars.
|
||||
|
||||
## Minimal Prompt For Next Agent
|
||||
|
||||
```text
|
||||
Continue acdream M1.5 indoor render in SAME worktree:
|
||||
C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b
|
||||
branch claude/thirsty-goldberg-51bb9b. Do NOT branch/worktree. Do NOT push. NEVER stash/gc.
|
||||
|
||||
The current dirty render code is not visually accepted. The user stopped the prior agent after repeated regressions.
|
||||
Read docs/research/2026-06-06-retail-pview-renderer-replacement-attempts-handoff.md first.
|
||||
|
||||
Current symptoms: transition flaps still happen indoor/outdoor, room/room, cellar; ground floor is now transparent; cellar broken; prior runs showed grey/black clear color, missing wall textures, and character cut on stairs.
|
||||
|
||||
Do not patch first. Audit dirty diff, read named retail PView, parse launch-flap-shell-capture-relaunch.log and launch-pview-watermark-probe.log. Determine exactly why:
|
||||
1) cellar root 0174 fails to traverse 0174 -> 0175 when proj=0;
|
||||
2) root 0172 toggles outside_view/0170 reachability while 0172 -> 016F has proj=0;
|
||||
3) shell/object/terrain/depth-clear decisions disagree.
|
||||
|
||||
Patch only the retail mismatch. Build/test before relaunch. Do not claim success before user visual confirmation.
|
||||
```
|
||||
|
||||
160
docs/research/2026-06-07-indoor-render-session-handoff.md
Normal file
160
docs/research/2026-06-07-indoor-render-session-handoff.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Indoor Render — Session Handoff: HANG fixed + interior SEALS; the FLAP is next — 2026-06-07
|
||||
|
||||
> Worktree `thirsty-goldberg-51bb9b`, branch `claude/thirsty-goldberg-51bb9b`. PowerShell on Windows;
|
||||
> launch logs UTF-16; build before launch; acceptance is the user's eyes. Live ACE `127.0.0.1:9000`,
|
||||
> `testaccount`/`testpassword`, char `+Acdream` (spawns near the Holtburg / "Arcanum" cottage —
|
||||
> landblock `0xA9B4`, cottage cells `0xA9B4016F–0175`). Do NOT branch/worktree, push, or `git stash`/`gc`.
|
||||
|
||||
## TL;DR
|
||||
|
||||
The two-week indoor-render **HANG is FIXED** and the interior **SEALS** (walls/floor/ceiling draw,
|
||||
textured) — both committed this session and live-verified by the user ("Ok now it runs!"). A
|
||||
structured live test pinned the remaining dominant visible issue, the **FLAP at transitions**, as
|
||||
**viewer-cell metastability**: the render roots at the camera-eye cell, which oscillates
|
||||
outdoor↔indoor as the 3rd-person boom drifts across the doorway plane. **The flap is a SEPARATE,
|
||||
already-designed fix — it is NOT the verbatim DrawCells port; finishing the port will not fix it.**
|
||||
Next session: **fix the flap** (camera-boom stability + viewer-cell dead-zone). Tracked follow-ups:
|
||||
#78 terrain gating, look-in-from-inside sealing, look-in FPS, L-spotlight.
|
||||
|
||||
## What shipped this session (committed — see `git log` on this branch)
|
||||
|
||||
### 1. The HANG fix (the blocker)
|
||||
Indoor frames froze (`AppHangB1`; not a crash — captured the spinning managed stack via a
|
||||
`dotnet-stack` hang-watcher). Root cause: `PortalVisibilityBuilder.Build`'s portal-visibility flood
|
||||
**did not terminate** for real cottage geometry. Two layers, two fixes (both kept):
|
||||
- **A — drift-tolerant `CellView.Add` dedup** (`src/AcDream.App/Rendering/PortalView.cs`). The flood
|
||||
re-queues a cell every time its view GROWS; growth only stops when the dedup recognises a re-clipped
|
||||
region as a duplicate. The faithful `ProjectToClip` near-side clip drifts per round, so the old
|
||||
exact index-by-index match (eps 1e-4) never caught the near-duplicate → unbounded growth → O(n²)
|
||||
CPU-spin in `CellView.Add`. Fix: key each polygon by its vertices **snapped to a 1e-3 NDC grid**,
|
||||
consecutive-dedup'd, **canonically rotated** to a lex-min start → finite key space → convergence.
|
||||
Tests: `tests/AcDream.App.Tests/Rendering/CellViewDedupTests.cs` (3).
|
||||
- **B — bounded re-enqueue** (`src/AcDream.App/Rendering/PortalVisibilityBuilder.cs`). A alone did not
|
||||
fully converge (the spin relocated to `ScreenPolygonClip.ClipByEdge` — bounded loops — inside the
|
||||
still-non-terminating BFS). Restored the **`MaxReprocessPerCell = 16`** hard cap that Phase U.2a
|
||||
deleted ("fixpoint termination" left the loop with NO bound). **Kept the re-enqueue** — it is
|
||||
load-bearing for late-slice propagation (`Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit`).
|
||||
Pure enqueue-once was tried and **broke that test**, so re-enqueue is kept and merely bounded.
|
||||
- Deep diagnosis + the reassessment that led to B: `docs/research/2026-06-06-indoor-render-hang-rootcause.md`.
|
||||
- **Verified:** clean exit (255→0); runs indoors with no freeze; the indoor flood converges in ~1
|
||||
round/cell at normal positions (measured 3–5 pops/frame, 1 view-poly/cell). The cap only bites at
|
||||
the metastable doorway.
|
||||
|
||||
### 2. The SEAL (verbatim DrawCells port — Task 2)
|
||||
`RetailPViewRenderer.DrawEnvCellShells` now iterates `IndoorDrawPlan.ShellPass(pvFrame)` — **every**
|
||||
visible cell's shell draws (was gated on `ClipFrameAssembler`'s slot filter → cells without a slot
|
||||
were silently dropped → grey clear-color void). Verified: interior seals + textured. (Task 1
|
||||
`IndoorDrawPlan` + its test committed earlier as `bff1955`.)
|
||||
|
||||
### 3. Look-in FPS
|
||||
`GameWindow` exterior-look-in candidate cells limited to the player's landblock **±1** (was **all
|
||||
~81 loaded landblocks** iterated every outdoor frame just to discard them via the 48 m seed cutoff).
|
||||
Provably no behavior change (excluded cells are >48 m, already culled). Outdoor FPS improved but
|
||||
still **~110 fps / ~9 ms (was ~200)** — `DrawPortal` still draws ~12 building interiors/frame (see
|
||||
follow-up).
|
||||
|
||||
## Baselines (must hold at next session start)
|
||||
- `dotnet build -c Debug` **0 errors**.
|
||||
- App.Tests **210/210** (205 baseline + IndoorDrawPlanTests 2 + CellViewDedupTests 3).
|
||||
- Core.Tests **1331 pass / 4 fail / 1 skip** — the 4 are pre-existing Physics door/step-up, unrelated.
|
||||
|
||||
## Structured live test — findings (Holtburg/Arcanum cottage, 2026-06-07)
|
||||
User walked a 6-step protocol (inside-still → camera-pan → doorway-threshold → just-outside →
|
||||
looking-at-cottage → cellar) and reported 8 behaviours; `ACDREAM_PROBE_FLAP` `[render-sig]`
|
||||
correlated each.
|
||||
|
||||
| # | Observed | Cause | Bucket |
|
||||
|---|---|---|---|
|
||||
| 2,3,6,8 | walls briefly transparent / window+entrance "covered by the world background" / abrupt "teleport" through the doorway — all **at transitions (camera crossing a threshold)** | **THE FLAP** | viewer-cell stability (NEXT) |
|
||||
| 1 | outdoor grass covers the cellar-entrance hole (steady, looking in from outside) | outdoor terrain not gated over the indoor floor opening | **#78** terrain gating |
|
||||
| 7 | from inside, a building seen through the doorway has transparent walls (world-bg shows); pops back when you step outside | look-out shows other buildings unsealed | look-in/look-out completeness |
|
||||
| 5 | spotlight blobs on textures from the ceiling lamp (always been there) | point-light artifact | **L-spotlight** (separate) |
|
||||
| FPS | inside very high; outside **110 fps / ~9 ms** (was ~200) | `DrawPortal` draws ~12 interiors/frame | look-in cost |
|
||||
| 4 | cellar transitions **stable** ✓ | vertical transition doesn't cross the outdoor boundary | — |
|
||||
|
||||
### The FLAP — pinned (render-sig evidence)
|
||||
`[render-sig]` over the doorway shows the render branch + the cell it roots at flip-flopping while the
|
||||
**player cell stays inside**:
|
||||
```
|
||||
50× branch=OutdoorRoot viewer=0xA9B40031 (outdoor) player=0xA9B40171 (indoor) gate=in
|
||||
16× branch=RetailPViewInside viewer=0xA9B40170 (indoor) player=0xA9B40171 gate=in
|
||||
113× branch=RetailPViewInside viewer=0xA9B40171 (indoor) player=0xA9B40171 gate=in
|
||||
... oscillates 0x0031 ↔ 0x0170 ↔ 0x0171 frame-to-frame ...
|
||||
```
|
||||
**Mechanism:** the render roots at the **viewer (camera-eye) cell** (`clipRoot = viewerRoot`, Phase W
|
||||
"one viewpoint"). The 3rd-person boom drifts the eye across the doorway plane; acdream re-resolves the
|
||||
viewer cell fresh each frame with **no hysteresis** → it flips between outdoor `0x0031` and indoor
|
||||
`0x0170/0x0171` → the render flips `OutdoorRoot`↔`RetailPViewInside` → the indoor seal drops (walls
|
||||
transparent, outdoor world/grass shows) then re-seals → **flapping**. This is exactly the 2026-06-05
|
||||
viewer-cell-flicker diagnosis, now confirmed against the live render branch.
|
||||
|
||||
## RECOMMENDED NEXT WORK — fix the FLAP (separate, already-designed)
|
||||
Per `docs/research/2026-06-05-viewer-cell-flicker-rootcause-and-fix-plan-handoff.md`, 3 retail-faithful parts:
|
||||
1. **Viewer-cell dead-zone (do this first)** — ±0.2 mm cell hysteresis so a sub-mm eye drift can't flip
|
||||
the cell (`PhysicsCameraCollisionProbe.SweepEye`; retail `point_inside_cell_bsp` 0x53c1f0). Highest
|
||||
leverage — likely kills most of the flap on its own.
|
||||
2. **Camera-boom stability** — stop the boom drifting at rest (`RetailChaseCamera.UpdateCamera`; retail
|
||||
`UpdateCamera` 0x456660).
|
||||
3. **w-space (w=0) portal clip** — close-portal projection degeneracy (`PortalProjection` /
|
||||
`PortalVisibilityBuilder`; retail `GetClip` 0x5a4320 / `polyClipFinish` 0x6b6d00). Lower priority.
|
||||
|
||||
Apparatus ready: `ACDREAM_PROBE_FLAP` emits `[render-sig]` (branch/viewer/player/gate per frame),
|
||||
`[flap]`, `[flap-cam]`, `[flap-sweep]` — light enough to launch with (the heavy `ACDREAM_PROBE_SHELL`
|
||||
firehose is what previously caused an I/O stall; avoid it).
|
||||
|
||||
## Tracked follow-ups (logged; not yet fixed)
|
||||
- **#78 terrain gating** — outdoor terrain (grass) draws over the indoor cellar-entrance hole (and likely
|
||||
other indoor floors). Decomp anchor `CEnvCell::find_visible_child_cell` (`acclient_2013_pseudo_c.txt:311397`).
|
||||
- **Look-in-from-inside** — buildings seen through your door/window from inside render unsealed
|
||||
(transparent walls); the look-out pass doesn't draw other buildings' shells. DrawCells port Task 5/7
|
||||
territory (or R2 "outside-looking-in").
|
||||
- **Look-in FPS** — `DrawPortal` draws ~12 building interiors every outdoor frame (~110 vs ~200 fps).
|
||||
Optimize: only look into buildings whose exit portals are frustum-visible; skip when no door is in view.
|
||||
- **L-spotlight** — ceiling-lamp point light makes spotlight blobs on textures. Pre-existing, separate.
|
||||
|
||||
## verbatim DrawCells port — remaining tasks (deferred)
|
||||
Plan: `docs/superpowers/plans/2026-06-06-verbatim-retail-indoor-render-port.md`. Task 1 + Task 2 done.
|
||||
**Task 3** (objects no-clip) is effectively already satisfied (objects draw membership-gated with no
|
||||
clip; no half-characters observed). **Tasks 4–8** (per-slice trim, look-out, delete `ClipFrameAssembler`,
|
||||
look-in, final) are **cleanup with no current visible payoff** — the seal works and there is **no visible
|
||||
bleed** (the "glitches between cells" were the FLAP, not bleed). **Task 4 (trim) is intricate** (its
|
||||
per-slice `_clipFrame.Reset()` is coupled with the landscape/particle passes that still read
|
||||
`clipAssembly` slots) and **risks re-slicing the working seal** — do it carefully, fresh, and only when
|
||||
clean architecture is the priority.
|
||||
|
||||
## DO NOT re-litigate
|
||||
- The HANG fix (A drift-dedup + B bounded re-enqueue) is correct + verified. **Do NOT try pure
|
||||
enqueue-once** — it breaks `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` (late-slice
|
||||
propagation needs the re-enqueue; the cap, not removal, is the termination guarantee).
|
||||
- The grey was the `drawableCells` / `ClipFrameAssembler` slot filter; Task 2 fixed it. The clip math is
|
||||
faithful — do not "harden the w-clip".
|
||||
- **The FLAP is NOT the DrawCells port.** It is viewer-cell metastability (camera/membership). Tasks 4–8
|
||||
will NOT fix it.
|
||||
- The render roots at the VIEWER (camera-eye) cell intentionally (Phase W "one viewpoint"). The flap fix
|
||||
is to STABILISE the viewer cell (dead-zone + boom), NOT to re-root at the player cell (superseded).
|
||||
|
||||
## Copy-paste pickup prompt (next session)
|
||||
```
|
||||
Pick up the indoor-render work in worktree thirsty-goldberg-51bb9b (branch
|
||||
claude/thirsty-goldberg-51bb9b). PowerShell; launch logs UTF-16; build before launch; acceptance is
|
||||
the user's eyes. Do NOT branch/worktree, push, git stash/gc, or revert the dirty tree.
|
||||
|
||||
Read first: docs/research/2026-06-07-indoor-render-session-handoff.md (state, what shipped, the FLAP
|
||||
diagnosis, do-not-relitigate). Then docs/research/2026-06-05-viewer-cell-flicker-rootcause-and-fix-plan-handoff.md
|
||||
(the flap fix plan).
|
||||
|
||||
Confirm baselines: build 0 errors; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.
|
||||
|
||||
The indoor HANG is fixed and the interior SEALS (shipped + committed last session). The remaining
|
||||
dominant visible issue is the FLAP at transitions — viewer-cell metastability: the render roots at the
|
||||
camera-eye cell, which oscillates outdoor↔indoor as the 3rd-person boom drifts across the doorway (no
|
||||
hysteresis), confirmed in [render-sig]. FIX THE FLAP, starting with the viewer-cell dead-zone
|
||||
(PhysicsCameraCollisionProbe.SweepEye; retail point_inside_cell_bsp 0x53c1f0), then camera-boom
|
||||
stability (RetailChaseCamera.UpdateCamera; retail UpdateCamera 0x456660). Launch with ACDREAM_PROBE_FLAP
|
||||
only (NOT ACDREAM_PROBE_SHELL — it stalls on I/O). Gate on the user's eyes at the cottage doorway.
|
||||
|
||||
Do NOT: retry pure enqueue-once (breaks late-slice propagation); re-root render at the player cell
|
||||
(viewer-cell rooting is intentional); finish DrawCells port Tasks 4-8 expecting it to fix the flap (it
|
||||
won't). Tracked follow-ups (not the flap): #78 terrain gating (grass over cellar hole), look-in-from-
|
||||
inside sealing, look-in FPS (DrawPortal ~12 interiors/frame), L-spotlight.
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue