docs(render): Phase W render-rewrite plan (Stages 3-5) — grounded, per-step
Per-step subagent-driven plan for the render half: T0 test-hygiene baseline, Stage 3 render-root unification (root at CellGraph.CurrCell + seen_outside, drop the FindCameraCell grace-frame fallback), Stage 4 PView seal (sky/landscape inside the portal-clip bracket + conditional doorway Z-clear = no blue-hole; EnvCellRenderer GL_BLEND verify), Stage 5 entity/particle cell-clip. Key reframe from grounding the plan in the actual code: the PView infra (PortalVisibilityBuilder BFS + OutsideView, ClipFrame, EnvCellRenderer GL_BLEND fix, WbDrawDispatcher cell gate) ALREADY EXISTS and the A8 stencil split is already gone — so the render half is wire-and-fill-gaps, not a from-scratch port. Execution policy: no intermediate user gates, single final visual verification, full suite green at verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
59f3a1380d
commit
a06226f9a2
1 changed files with 782 additions and 0 deletions
782
docs/superpowers/plans/2026-06-02-phase-w-render-rewrite.md
Normal file
782
docs/superpowers/plans/2026-06-02-phase-w-render-rewrite.md
Normal file
|
|
@ -0,0 +1,782 @@
|
|||
# Phase W Render Rewrite — Stages 3-5 Implementation Plan
|
||||
|
||||
**Goal:** Complete the render half of Phase W: root visibility at the physics `CurrCell`
|
||||
+ `seen_outside` (Stage 3), port the retail PView portal traversal producing one
|
||||
`cell_draw_list` + `OutsideView` that draws sky/rain through exit portals with no blue
|
||||
hole (Stage 4), and clip entities/particles to the PView visible set (Stage 5).
|
||||
Result: cottage interior is sealed, outdoor sky visible through the door, no transparent
|
||||
walls, no entity bleed — at both cottage and dungeon.
|
||||
|
||||
**Architecture (2-3 sentences):** Retail uses one cell graph and one portal-visibility
|
||||
traversal (`PView`) rooted at the committed player cell (`CellGraph.CurrCell`); the
|
||||
inside/outside decision is a single `(id & 0xFFFF) < 0x100` + `seen_outside` predicate,
|
||||
and landscape is drawn through exit-portal clip regions by `DrawCells`, not as a separate
|
||||
stencil pass. acdream already has `PortalVisibilityBuilder` (a faithful `PView` port),
|
||||
`EnvCellRenderer` (cell-shell draw), `ClipFrame`/`ClipFrameAssembler` (the outdoor clip
|
||||
machinery), and `CellGraph.CurrCell` (the physics membership answer) — Stage 3 deletes
|
||||
the `FindCameraCell` grace-frame path and wires `CurrCell` as the mandatory root; Stage 4
|
||||
uses the existing `OutsideView` planes to drive terrain + sky draw indoors; Stage 5 uses
|
||||
the `PortalVisibilityFrame.OrderedVisibleCells` set to clip entity/particle draws.
|
||||
|
||||
**Tech stack:** .NET 10 / C# / OpenGL 4.3 + bindless / Silk.NET. Tests in
|
||||
`tests/AcDream.Core.Tests/` (unit + replay) and `tests/AcDream.App.Tests/` (app-layer);
|
||||
no new test projects.
|
||||
|
||||
**REQUIRED SUB-SKILL:** `superpowers:subagent-driven-development` — each stage should
|
||||
be dispatched as a bounded implementation chunk with a clear spec and acceptance criteria.
|
||||
|
||||
**EXECUTION POLICY (user directive 2026-06-02):** NO intermediate user visual gates. The
|
||||
per-stage "visual gate" sections below are demoted to **internal build+test-green
|
||||
checkpoints** (the orchestrator launches/inspects only if a probe is needed to settle a
|
||||
question). The user performs a **single final visual verification** after ALL stages land,
|
||||
and the **full Core+App test suite must be GREEN** at that point (no broken/red tests).
|
||||
Interim states (e.g. Stage 3's full-screen sky before Stage 4 clips it) are acceptable
|
||||
because no user sees them before the final gate. The `DoorwayMembershipReplayTests` must
|
||||
also use a **committed** fixture (a trimmed subset of `doorway-capture.jsonl`), not the
|
||||
untracked 364K capture, so the suite stays green/portable (folded into T0).
|
||||
|
||||
---
|
||||
|
||||
## Prerequisite reading (implementer must read before coding)
|
||||
|
||||
Before any Stage 3+ work:
|
||||
1. `docs/superpowers/specs/2026-06-02-phase-w-transition-membership-and-pview-render-design.md`
|
||||
§2 (target architecture), §5 (risks), §6 (acceptance).
|
||||
2. This plan, from top to bottom.
|
||||
3. Current render loop: `GameWindow.cs` lines 7139–7513 — the visibility, terrain, entity,
|
||||
and particle draws. Confirm line numbers before editing (they shift with every commit).
|
||||
4. `src/AcDream.App/Rendering/CellVisibility.cs` — `FindCameraCell` (line 389), grace
|
||||
counter (line 214), `ComputeVisibilityFromRoot` (line 356), `GetVisibleCellsFromRoot`
|
||||
(line 539).
|
||||
5. `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` — BFS + `OutsideView` handling
|
||||
(lines 1–239).
|
||||
|
||||
---
|
||||
|
||||
## File structure (created / modified)
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|------|--------|----------------|
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs` | Modify (lines ~7139–7513) | Stage 3: remove `FindCameraCell` grace-frame fallback; unify the indoor/outdoor gate; Stage 4: drive terrain+sky from `OutsideView` indoors; Stage 5: entity/particle clip. |
|
||||
| `src/AcDream.App/Rendering/CellVisibility.cs` | Modify (lines ~389–446) | Stage 3: delete or stub the AABB `FindCameraCell` grace-frame band-aid; promote `ComputeVisibilityFromRoot` to the sole path; expose `SeenOutside` of the root cell. |
|
||||
| `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` | Possibly extend | Stage 4: confirm `OutsideView` polygons are already propagated for the no-blue-hole case; add landscape-viewpoint helper if absent. |
|
||||
| `tests/AcDream.Core.Tests/Rendering/CellGraphRootTests.cs` | Create | Stage 3 unit tests: root-selection logic + `seen_outside` terrain/sky gate. |
|
||||
| `tests/AcDream.Core.Tests/Rendering/PViewBfsTests.cs` | Create or extend | Stage 4 unit tests: `OutsideView` non-empty for a cell with an exit portal; sealed-dungeon `OutsideView`-empty. |
|
||||
| `tests/AcDream.Core.Tests/Rendering/EntityClipTests.cs` | Create | Stage 5: cell-clip predicate unit tests. |
|
||||
|
||||
> **No new production-code files** except the three new test files above. All render changes
|
||||
> are surgical edits to existing files.
|
||||
|
||||
---
|
||||
|
||||
## Test-hygiene task (run FIRST, before any Stage 3 code)
|
||||
|
||||
**Purpose:** establish a deterministic green baseline so Stage 3–5 additions don't mask
|
||||
pre-existing noise.
|
||||
|
||||
### Pre-existing known failures (documented; do NOT fix unless noted)
|
||||
|
||||
| Test class | Failure kind | Root cause | Action |
|
||||
|---|---|---|---|
|
||||
| `PhysicsResolveCapture` statics | 8–19 flaky failures per run | Static env-var read shared across test classes in the same process | Fix test isolation (see task T0.1 below). |
|
||||
| `CellarUpTrajectoryReplayTests.LiveCompare_FirstCap_FixClosesCottageFloorCap` | "document-the-bug" — passes while bug exists | Documents the cottage-floor cap; the Stage 2 membership fix is expected to make this test flip to FAILING, which means the bug is now fixed. Then update it to assert the fixed behavior. | After Stage 2 lands, update this test to assert the fix. |
|
||||
| `DoorBugTrajectoryReplayTests.Apparatus_Grounded_50cmOffCenter_FrontApproach_DocumentsBug` | Same pattern | Documents the door squeeze-through | After Stage 4 seals the pipeline, revisit and update to assert fixed behavior if applicable. |
|
||||
| `BSPStepUpTests` (some variants) | Intermittent | Static BSP state | Same static-isolation fix. |
|
||||
|
||||
### T0.1 — Fix static-leak test isolation
|
||||
|
||||
- [ ] **Find the static fields causing test leakage.** Search for `static readonly` or
|
||||
`static` initializers in `PhysicsResolveCapture.cs` and `PhysicsDiagnostics.cs` that
|
||||
read env vars at class-init time (before any test sets them). The symptom is that a test
|
||||
class that sets `ACDREAM_CAPTURE_RESOLVE` or `ACDREAM_PROBE_*` poisons the shared
|
||||
statics for downstream test classes in the same process.
|
||||
- Files: `src/AcDream.Core/Physics/PhysicsResolveCapture.cs`,
|
||||
`src/AcDream.Core/Physics/PhysicsDiagnostics.cs`.
|
||||
- Pattern to find: `static readonly bool s_xxx = Environment.GetEnvironmentVariable(...)`.
|
||||
|
||||
- [ ] **Add `[Collection(nameof(PhysicsResolveCapture))]` isolation OR convert static
|
||||
env-var reads to instance-time reads with a reset method.** The simplest retail-faithful
|
||||
fix: add a `ResetForTest()` static method that re-reads all flags, call it in
|
||||
`IDisposable.Dispose` of any test class that sets env vars. Alternatively annotate the
|
||||
affected test classes with `[Collection("IsolatedPhysicsCapture")]` to force sequential
|
||||
execution (xUnit's isolation model).
|
||||
- MUST NOT change production behavior — only test teardown.
|
||||
|
||||
- [ ] **Run `dotnet test -c Debug` twice in a row and confirm the failure set is
|
||||
deterministic** (same tests, same count, or fully green). The pre-existing documented-bug
|
||||
tests (`LiveCompare_FirstCap_FixClosesCottageFloorCap`, etc.) are allowed to remain red
|
||||
until their stage lands — record their names in a comment so Stage 4 implementers know
|
||||
to flip them.
|
||||
|
||||
Commit: `test: fix PhysicsResolveCapture static-leak isolation`
|
||||
|
||||
---
|
||||
|
||||
## Stage 3 — Render-root unification
|
||||
|
||||
**Goal:** root render visibility at `CellGraph.CurrCell` + `seen_outside`; remove the
|
||||
AABB `FindCameraCell` grace-frame source-of-truth; port the `CellManager::ChangePosition`
|
||||
landscape/sky policy; camera offset via graph child lookup. Visual gate: no render-branch
|
||||
strobe; landscape policy correct indoors/outdoors/dungeon.
|
||||
|
||||
**Retail anchors:**
|
||||
- `SmartBox::RenderNormalMode @ 0x00453aa0 (pseudo_c:92635)` — the single indoor/outdoor
|
||||
decision (viewer landcell → `LScape::draw`; viewer EnvCell → `DrawInside`).
|
||||
- `CellManager::ChangePosition @ 0x004559b0(pseudo_c:94601)` — landscape kept live iff
|
||||
`seen_outside || isLandCell`.
|
||||
- `CEnvCell::find_visible_child_cell @ 0x0052dc50 (pseudo_c:311397)` — camera-offset
|
||||
child cell lookup (rooted at the player cell, not the eye).
|
||||
|
||||
**Key current-code map:**
|
||||
- `GameWindow.cs:7162–7166` — `physicsRoot` derivation from `CellGraph.CurrCell`; already
|
||||
exists but only used as a FALLBACK to `ComputeVisibility`.
|
||||
- `GameWindow.cs:7166` — `ComputeVisibilityFromRoot` call (already the main path via W2a).
|
||||
- `GameWindow.cs:7185–7187` — `playerInsideCell` computation (calls `IsInsideAnyCell`,
|
||||
which is an independent AABB scan — this is a separate issue from `FindCameraCell` but
|
||||
related).
|
||||
- `GameWindow.cs:7267` — `bool renderSky = !cameraInsideCell` — the current sky gate,
|
||||
keyed off `visibility?.CameraCell is not null`.
|
||||
- `GameWindow.cs:7406` — `if (terrainClipMode == TerrainClipMode.Skip)` — the current
|
||||
terrain gate (no draw when `Skip`).
|
||||
- `CellVisibility.cs:389` — `FindCameraCell` (AABB, with grace-frame).
|
||||
- `CellVisibility.cs:214` — `CellSwitchGraceFrameCount = 3`.
|
||||
- `CellVisibility.cs:356` — `ComputeVisibilityFromRoot` — already the production path.
|
||||
|
||||
### T3.1 — Eliminate the `FindCameraCell` fallback path
|
||||
|
||||
**Current code (CellVisibility.cs, ~line 356-369):**
|
||||
```csharp
|
||||
public VisibilityResult? ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos)
|
||||
{
|
||||
if (root is null)
|
||||
return ComputeVisibility(fallbackPos); // <-- this calls FindCameraCell(fallbackPos)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Read `CellVisibility.cs:356–370` and `ComputeVisibility` (line ~521-527)** to
|
||||
confirm the fallback chain: `ComputeVisibilityFromRoot(null, pos)` calls
|
||||
`ComputeVisibility(pos)` which calls `FindCameraCell(pos)` with the AABB + grace-frame
|
||||
logic.
|
||||
|
||||
- [ ] **Change `ComputeVisibilityFromRoot` so a null `root` returns `null` instead of
|
||||
calling `ComputeVisibility`.** The caller at `GameWindow.cs:7166` already handles a
|
||||
null result as the "outdoor root" path (no interior portal frame, everything slot 0).
|
||||
The fallback was the old bridge for the transition from `FindCameraCell` to
|
||||
`CurrCell`-based root; it is now dead because W2a ensures `CurrCell` is set from the
|
||||
first tick.
|
||||
```csharp
|
||||
// BEFORE (CellVisibility.cs:~359-361):
|
||||
if (root is null)
|
||||
return ComputeVisibility(fallbackPos);
|
||||
// AFTER:
|
||||
if (root is null)
|
||||
return null; // outdoor root: caller handles null as "player is outside"
|
||||
```
|
||||
|
||||
- [ ] **Confirm the fallback for `physicsRoot == null` in `GameWindow.cs:7162–7166`:**
|
||||
when `CellGraph.CurrCell` is null (pre-spawn), `physicsRoot` is null, and
|
||||
`ComputeVisibilityFromRoot(null, …)` returns null — the outdoor path runs, which is
|
||||
correct (pre-spawn, no indoor draws). No additional change needed here.
|
||||
|
||||
- [ ] **Delete (or comment-out-for-reference, then delete in the same commit) the
|
||||
`FindCameraCell` method** (`CellVisibility.cs:389–446`) and the grace-frame counter
|
||||
(`_cellSwitchGraceFrames` field, `_lastCameraCell` field). If `ComputeVisibility` (the
|
||||
non-root variant, line 521) has other callers, check via Grep first — if it is ONLY
|
||||
called from `ComputeVisibilityFromRoot`, delete it too; otherwise stub it to call
|
||||
`GetVisibleCellsFromRoot(null, …)` and note the debt.
|
||||
|
||||
**Confirm in Step 1:** search for all callers of `FindCameraCell` and
|
||||
`ComputeVisibility` in the App project before deleting. Expected: zero callers outside
|
||||
`CellVisibility.cs` itself and the one `GameWindow.cs` chain. If any other callers exist
|
||||
note them and do not delete until they are migrated.
|
||||
|
||||
Commit: `refactor(render): Stage 3 — delete FindCameraCell AABB grace-frame fallback`
|
||||
|
||||
---
|
||||
|
||||
### T3.2 — Port `seen_outside` terrain/sky gate (retail `CellManager::ChangePosition`)
|
||||
|
||||
**The retail policy (verified, `pseudo_c:94649`):**
|
||||
```
|
||||
if (seen_outside || keep_lscape_loaded)
|
||||
keep landscape + terrain
|
||||
else
|
||||
LScape::release_all (dungeon)
|
||||
```
|
||||
|
||||
The current acdream code uses `cameraInsideCell` (any non-null `CameraCell` from
|
||||
`ComputeVisibilityFromRoot`) to gate both sky and terrain. This has two problems:
|
||||
1. It gates sky/terrain on "camera is inside ANY cell" — not on `seen_outside`. A dungeon
|
||||
cell has `SeenOutside = false` but `cameraInsideCell = true`, and the current code
|
||||
suppresses sky (correct for dungeon) but does NOT actively gate terrain for a dungeon
|
||||
that has accidentally non-empty `OutsideView` (edge case).
|
||||
2. Sky and terrain should be gated SEPARATELY: `seen_outside` controls whether terrain is
|
||||
available at all; the `OutsideView` from the BFS controls whether it draws THIS frame.
|
||||
|
||||
- [ ] **Extract `seenOutside` from the PVS root cell.** After the
|
||||
`ComputeVisibilityFromRoot` call at `GameWindow.cs:7166`, add:
|
||||
```csharp
|
||||
// Retail CellManager::ChangePosition:94649 — keep landscape iff seen_outside.
|
||||
bool rootSeenOutside = physicsRoot?.SeenOutside ?? true; // outdoor root (null) → always seen_outside
|
||||
```
|
||||
(`LoadedCell.SeenOutside` is already populated at `GameWindow.cs:5718` from
|
||||
`envCell.Flags.HasFlag(EnvCellFlags.SeenOutside)`.)
|
||||
|
||||
- [ ] **Replace the `playerInsideCell` AABB scan with `seenOutside`-derived logic.**
|
||||
Currently (`GameWindow.cs:7185–7187`):
|
||||
```csharp
|
||||
bool playerInsideCell = (_playerMode && _playerController is not null)
|
||||
? _cellVisibility.IsInsideAnyCell(_playerController.Position)
|
||||
: cameraInsideCell;
|
||||
```
|
||||
This calls `IsInsideAnyCell` (brute-force AABB scan of all cells every frame). Replace
|
||||
with: `bool playerInsideCell = cameraInsideCell && !rootSeenOutside;` — i.e., player
|
||||
is "fully indoor" (no sky/sun) only when inside a cell AND that cell cannot see outside
|
||||
(a dungeon). Building interiors with `seen_outside` keep the sun because the sky is
|
||||
visible through the door. Document the retail anchor (`CellManager::ChangePosition
|
||||
@ 0x004559b0, pseudo_c:94649`). **Confirm in Step 1**: verify that `UpdateSunFromSky`
|
||||
(called at `GameWindow.cs:7202`) behaves correctly with this new semantics — `true` =
|
||||
kill sunlight (dungeon); `false` = keep sunlight (outdoor or building interior).
|
||||
If the sun logic inverts this flag, invert accordingly.
|
||||
|
||||
- [ ] **Replace `bool renderSky = !cameraInsideCell` (`GameWindow.cs:7267`) with:**
|
||||
```csharp
|
||||
// Sky suppressed when inside a sealed interior (no exit portals). Building interiors
|
||||
// with seen_outside still draw sky — it appears through the doorway via OutsideView
|
||||
// (Stage 4). Outdoor root: always render sky. Retail RenderNormalMode:92649.
|
||||
bool renderSky = !cameraInsideCell || rootSeenOutside;
|
||||
```
|
||||
For a building interior: `cameraInsideCell = true`, `rootSeenOutside = true` →
|
||||
`renderSky = true` (sky is drawn, clipped to the doorway by Stage 4).
|
||||
For a dungeon: `cameraInsideCell = true`, `rootSeenOutside = false` →
|
||||
`renderSky = false`.
|
||||
For outdoor: `cameraInsideCell = false` → `renderSky = true`.
|
||||
**Note:** This means sky now renders indoors for `seen_outside` cells. Until Stage 4
|
||||
is landed, sky will draw full-screen in building interiors (wrong but expected interim
|
||||
regression). Stage 4's `OutsideView` clipping will confine it to the doorway opening.
|
||||
Document this as an expected interim state; do not gate on a TODO flag.
|
||||
|
||||
- [ ] Similarly, update the weather gate at `GameWindow.cs:7506` (`if (!cameraInsideCell)`)
|
||||
to use `renderSky` instead, so rain also follows the same policy.
|
||||
|
||||
Commit: `feat(render): Stage 3 — seen_outside terrain/sky gate per CellManager::ChangePosition`
|
||||
|
||||
---
|
||||
|
||||
### T3.3 — Camera-offset child-cell lookup (retail `find_visible_child_cell`)
|
||||
|
||||
**Context:** In 3rd-person chase mode, the camera eye drifts outside the player cell
|
||||
(U.4c). The render root is already pinned to the player cell (`visRootPos` at
|
||||
`GameWindow.cs:7153`). This task ensures that when the camera is slightly outside the
|
||||
player's cell, we use a graph/BSP child lookup rather than an AABB reclassification to
|
||||
find the camera's cell for the projection.
|
||||
|
||||
**Retail anchor:** `CEnvCell::find_visible_child_cell @ 0x0052dc50 (pseudo_c:311397)` —
|
||||
walks the cell's `stab_list` looking for the first cell whose `PointInCell` contains the
|
||||
query point. It is used by `AdjustPosition` (pc:280028) for the camera position, not a
|
||||
fresh AABB scan.
|
||||
|
||||
**Current state:** `GameWindow.cs:7153–7165` already roots the BFS at the PLAYER cell
|
||||
(U.4c). The AABB `FindCameraCell` (now deleted by T3.1) is the old camera-cell resolver.
|
||||
After T3.1, there is no AABB camera reclassification. The player's `CurrCell` is used as
|
||||
the PVS root, and that is all we need for the portal traversal.
|
||||
|
||||
- [ ] **Add `CellGraph.FindVisibleChildCell(Vector3 worldPoint)` to `CellGraph.cs`
|
||||
(new method, ~10 lines):**
|
||||
```csharp
|
||||
/// Retail find_visible_child_cell (pseudo_c:311397): walk CurrCell's StabList + self,
|
||||
/// return the first EnvCell whose PointInCell is true for worldPoint.
|
||||
/// Used to resolve the camera cell in 3rd-person from the physics cell graph,
|
||||
/// NOT from a fresh AABB scan.
|
||||
public EnvCell? FindVisibleChildCell(uint rootId, Vector3 worldPoint)
|
||||
{
|
||||
if (!_envCells.TryGetValue(rootId, out var root)) return null;
|
||||
if (root.PointInCell(worldPoint)) return root;
|
||||
foreach (var stabId in root.StabList)
|
||||
if (_envCells.TryGetValue(stabId, out var stab) && stab.PointInCell(worldPoint))
|
||||
return stab;
|
||||
return null;
|
||||
}
|
||||
```
|
||||
File: `src/AcDream.Core/World/Cells/CellGraph.cs` (append after `GetVisible`).
|
||||
|
||||
- [ ] **Wire it in `GameWindow.cs`**: After W2a lands, the only use of `FindCameraCell`
|
||||
was for the `physicsRoot` FALLBACK. That fallback is now deleted (T3.1). The
|
||||
`FindVisibleChildCell` is used for the VISUAL projection override when the chase camera
|
||||
is outside the player cell — for example, to drive `envCellViewProj` from the correct
|
||||
cell for portal-side tests in `PortalVisibilityBuilder.Build`. Currently `visRootPos`
|
||||
(the player pos) is passed as the side-test anchor (already correct per U.4c). This
|
||||
task is **low-risk**: the projection is driven from `camera.View * camera.Projection`
|
||||
(the actual eye), which is unchanged. The side-test anchor (`visRootPos`) is already
|
||||
the player pos. **Confirm in Step 1:** verify no current code site calls
|
||||
`FindCameraCell` for the camera projection. If there is none, this T3.3 may reduce to
|
||||
the `FindVisibleChildCell` method addition only (needed by Stage 4's camera-outside-door
|
||||
scenario).
|
||||
|
||||
Commit: `feat(core): Stage 3 — CellGraph.FindVisibleChildCell (retail find_visible_child_cell)`
|
||||
|
||||
---
|
||||
|
||||
### T3.4 — Unit tests for render-root selection
|
||||
|
||||
File: `tests/AcDream.Core.Tests/Rendering/CellGraphRootTests.cs` (new).
|
||||
|
||||
- [ ] **Test: `RootSelection_OutdoorRoot_NullCurrCell_ReturnsFalseSeenOutside`**
|
||||
When `CurrCell == null` (pre-spawn), `seenOutside = true` (outdoor default),
|
||||
`renderSky = true`, `playerInsideCell = false`.
|
||||
|
||||
- [ ] **Test: `RootSelection_BuildingInterior_SeenOutside_SkyRendered`**
|
||||
`CurrCell = EnvCell with SeenOutside=true` → `rootSeenOutside = true` →
|
||||
`renderSky = true`, `playerInsideCell = false`.
|
||||
|
||||
- [ ] **Test: `RootSelection_Dungeon_NoSeenOutside_SkyNotRendered`**
|
||||
`CurrCell = EnvCell with SeenOutside=false` → `rootSeenOutside = false` →
|
||||
`renderSky = false`, `playerInsideCell = true`.
|
||||
|
||||
- [ ] **Test: `FindVisibleChildCell_PlayerCellContains_ReturnsPlayerCell`**
|
||||
Player in cell A; query point inside A → returns A.
|
||||
|
||||
- [ ] **Test: `FindVisibleChildCell_StabListContains_ReturnsNeighbour`**
|
||||
Player in cell A with StabList=[B]; query point outside A but inside B → returns B.
|
||||
|
||||
- [ ] **Test: `FindVisibleChildCell_NeitherContains_ReturnsNull`**
|
||||
Query point outside all cells → null.
|
||||
|
||||
These tests are pure-logic (no GL). Run: `dotnet test --filter
|
||||
"FullyQualifiedName~CellGraphRootTests" -c Debug`.
|
||||
|
||||
Commit: `test(render): Stage 3 — CellGraphRootTests`
|
||||
|
||||
---
|
||||
|
||||
### Stage 3 visual gate
|
||||
|
||||
After T3.1–T3.4 green + `dotnet build` green:
|
||||
|
||||
> Ask the user to launch the client, walk to the Holtburg cottage, enter it, and confirm:
|
||||
> - **Outdoor**: terrain + sky draw as before. No regression.
|
||||
> - **Building interior (seen_outside=true)**: walls/floors render; sky may draw
|
||||
> full-screen (interim regression until Stage 4); no strobe between indoor/outdoor state.
|
||||
> - **Dungeon** (if accessible): no sky, no terrain; walls render.
|
||||
> - Cell ping-pong `0170↔0031` is gone (already fixed by Stages 1–2; confirm no
|
||||
> regression).
|
||||
|
||||
---
|
||||
|
||||
## Stage 4 — PView traversal + seamless seal
|
||||
|
||||
**This is the big one.** Goal: draw landscape through exit portals clipped to the doorway
|
||||
(kills the blue hole); seal ceilings; fix the `EnvCellRenderer` GL_BLEND regression.
|
||||
Visual gate: interior sealed, sky/rain through the door, no blue-hole, no transparent walls.
|
||||
|
||||
**Retail anchors:**
|
||||
- `PView::ConstructView @ 0x005a57b0 (pseudo_c:433750)` — BFS, already ported to
|
||||
`PortalVisibilityBuilder.Build`.
|
||||
- `PView::DrawCells @ 0x005a4840 (pseudo_c:432709)` — `if outside_view.view_count > 0 →
|
||||
LScape::draw first; conditional Z-clear (NOT color); then draw indoor cells`.
|
||||
- `PView::DrawInside @ 0x005a5860 (pseudo_c:433793)` — top-level indoor entry; calls
|
||||
`ConstructView` then `DrawCells`.
|
||||
- `PView::ClipPortals @ 0x005a5520 (pseudo_c:433662)` — exit portal →
|
||||
`outside_view.view_count += 1`, clip region registered.
|
||||
- `SmartBox::RenderNormalMode @ 0x00453aa0 (pseudo_c:92635,92667)` — if `seen_outside`,
|
||||
call `LScape::update_viewpoint(get_outside_cell_id(&viewer))` BEFORE `DrawInside`.
|
||||
|
||||
**Current state:**
|
||||
- `PortalVisibilityBuilder.Build` already produces `frame.OutsideView` with exit-portal
|
||||
clip polygons (`PortalVisibilityBuilder.cs:163–175`).
|
||||
- `ClipFrameAssembler.Assemble` already extracts `OutsideView` planes and calls
|
||||
`clipFrame.SetTerrainClip(outsidePlanes)` when planes exist (the binding=2 terrain UBO).
|
||||
- `GameWindow.cs:7406–7431` already has the `TerrainClipMode.Skip/Scissor/Planes` terrain
|
||||
gate — terrain is SKIPPED when the player is indoor and `OutsideView` is empty.
|
||||
- **GAP:** When `OutsideView` is non-empty (exit portal visible from inside), terrain and
|
||||
sky should draw (clipped to the doorway). Currently sky is still suppressed by
|
||||
`renderSky = !cameraInsideCell` (fixed in T3.2), but the terrain is gated correctly
|
||||
(Planes mode if OutsideView has planes). However, sky is drawn BEFORE the portal frame
|
||||
is assembled, so even with T3.2's `renderSky = true`, the sky draws FULL-SCREEN before
|
||||
the clip bracket — it is not clipped to the doorway opening.
|
||||
|
||||
### T4.1 — Move sky draw inside the portal-clip bracket
|
||||
|
||||
**Problem:** Sky (`_skyRenderer?.RenderSky`) draws at `GameWindow.cs:7268–7275`, BEFORE
|
||||
the clip bracket (`glEnable(ClipDistance)` at line ~7387). This means sky is never
|
||||
clipped by the `gl_ClipDistance` planes, so it bleeds full-screen indoors.
|
||||
|
||||
**Fix:** Move the sky draw to AFTER the terrain draw but BEFORE the entity draw, inside
|
||||
the clip bracket. Sky must write to depth so entities z-test against it.
|
||||
|
||||
- [ ] **Read `GameWindow.cs:7255–7292`** (sky draw block) and `7377–7390` (clip bracket
|
||||
open). Confirm current order: sky → `IsLiveModeWaitingForLogin` goto → clip bracket
|
||||
opens → terrain → cells → entities.
|
||||
|
||||
- [ ] **Move `_skyRenderer?.RenderSky(...)` and the `SkyPreScene` particle pass to
|
||||
inside the clip bracket**, just before the terrain draw (`GameWindow.cs:~7405`).
|
||||
Structure becomes:
|
||||
```
|
||||
[clip bracket opens: glEnable(ClipDistance)]
|
||||
sky draw (RenderSky + SkyPreScene particles) — now inside clip so gated to OutsideView
|
||||
terrain draw
|
||||
EnvCellRenderer opaque
|
||||
entity dispatcher
|
||||
EnvCellRenderer transparent
|
||||
[clip bracket closes]
|
||||
particles (scene pass, not sky)
|
||||
weather + SkyPostScene (still outside-gated by renderSky)
|
||||
```
|
||||
**Confirm in Step 1:** verify `gl_ClipDistance` writes in `sky.vert` — if the sky
|
||||
shader does NOT write `gl_ClipDistance[i]`, enabling clip while sky draws is harmless
|
||||
(undefined behavior for an unwritten clip plane yields no clipping per the spec note
|
||||
in the code comment at `GameWindow.cs:~1097`). However, for the sky to be clipped TO
|
||||
the doorway we NEED the sky shader to write `gl_ClipDistance`. Check
|
||||
`src/AcDream.App/Rendering/Shaders/sky.vert` — if it does not write clip distances,
|
||||
add them mirroring `mesh_modern.vert`'s pattern. If that is too large a shader change
|
||||
for Stage 4, use the Scissor fallback: before the sky draw, set a scissor rectangle
|
||||
from `clipAssembly.TerrainScissorNdcAabb` (already computed for terrain), draw sky,
|
||||
then disable scissor. The scissor approach is simpler and works without shader changes.
|
||||
|
||||
Commit: `feat(render): Stage 4 — move sky draw inside portal-clip bracket`
|
||||
|
||||
---
|
||||
|
||||
### T4.2 — Landscape viewpoint pre-position (retail `LScape::update_viewpoint`)
|
||||
|
||||
**Retail:** Before `DrawInside`, when `seen_outside`, retail calls
|
||||
`LScape::update_viewpoint(lscape, Position::get_outside_cell_id(&viewer))` (`RenderNormalMode:92667`).
|
||||
This sets the terrain system's viewpoint to the OUTDOOR landcell corresponding to the
|
||||
indoor camera position — so the terrain, when drawn through the doorway, is correctly
|
||||
positioned.
|
||||
|
||||
**Current state:** Terrain draw at `GameWindow.cs:7425/7430` calls
|
||||
`_terrain?.Draw(camera, frustum, …)`. The terrain system's viewpoint is managed by
|
||||
`LandblockStreamer` based on the player's landblock (streaming center). This is
|
||||
separate from the per-frame projection the terrain shader sees.
|
||||
|
||||
- [ ] **Confirm in Step 1:** does `TerrainModernRenderer.Draw` use the camera's
|
||||
`view * projection` matrix from the `camera` argument, or a separately-tracked
|
||||
"landscape viewpoint"? If the shader reads the matrix from the scene UBO (which has
|
||||
the real camera view-proj), no change is needed — the terrain already projects from the
|
||||
correct eye position. If there is a separate "landscape eye" or terrain-specific
|
||||
viewpoint that gets reset to the player position rather than the camera position, it
|
||||
needs to be set to `Position.get_outside_cell_id(&viewer)` equivalent before the draw.
|
||||
Most likely acdream does NOT have this divergence (the terrain uses `camera.View *
|
||||
camera.Projection` just like everything else), so this task may be a no-op. Confirm
|
||||
and either add a `// verified: no viewpoint override needed` comment or fix it.
|
||||
|
||||
Commit (or no-op annotation): `docs(render): Stage 4 — verify terrain viewpoint is camera-relative`
|
||||
|
||||
---
|
||||
|
||||
### T4.3 — Conditional Z-clear for the doorway (retail `DrawCells:432731`)
|
||||
|
||||
**Retail:** After drawing the landscape (terrain + sky), retail does a CONDITIONAL Z-clear:
|
||||
```c
|
||||
if (forceClear || D3DPolyRender::portalsDrawnCount != 0)
|
||||
RenderDevice->Clear(4, 0x820fc0, 1.0); // flag 4 = Z-buffer ONLY, NOT color
|
||||
```
|
||||
This clears depth ONLY WHERE the portal geometry was drawn (exit portal polygons), so
|
||||
indoor geometry draws on top of the landscape without z-fighting through the doorway.
|
||||
It is a Z-clear, NOT a color-clear — so there is no "blue hole" painted.
|
||||
|
||||
**Current state:** acdream does not perform this clear. The current terrain `TerrainClipMode.Skip`
|
||||
path means terrain never draws indoors, so z-fighting has not been observed. Once terrain
|
||||
draws through the doorway (T4.1 fixed), z-fighting may appear at the doorway plane.
|
||||
|
||||
- [ ] **After the terrain/sky draw block (after T4.1's sky+terrain), add a conditional
|
||||
depth-only clear gated on `clipAssembly is not null && frame.OutsideView.Polygons.Count > 0`:**
|
||||
```csharp
|
||||
// Retail PView::DrawCells:432731 — conditional Z-clear after landscape, before indoor geometry.
|
||||
// Clears depth in the doorway region so indoor walls draw over the terrain.
|
||||
// Z-buffer only (glClear(GL_DEPTH_BUFFER_BIT)) — NOT color — so no blue hole.
|
||||
if (clipAssembly is not null && pvFrame.OutsideView.Polygons.Count > 0)
|
||||
{
|
||||
// Scissor to the doorway AABB before clearing to avoid clearing depth for
|
||||
// the entire screen (only the exit-portal region needs it).
|
||||
var fb = _window!.FramebufferSize;
|
||||
float nx0 = System.Math.Clamp(terrainScissorNdc.X, -1f, 1f);
|
||||
float ny0 = System.Math.Clamp(terrainScissorNdc.Y, -1f, 1f);
|
||||
float nx1 = System.Math.Clamp(terrainScissorNdc.Z, -1f, 1f);
|
||||
float ny1 = System.Math.Clamp(terrainScissorNdc.W, -1f, 1f);
|
||||
int px = (int)System.MathF.Floor((nx0 * 0.5f + 0.5f) * fb.X);
|
||||
int py = (int)System.MathF.Floor((ny0 * 0.5f + 0.5f) * fb.Y);
|
||||
int pw = (int)System.MathF.Ceiling((nx1 - nx0) * 0.5f * fb.X);
|
||||
int ph = (int)System.MathF.Ceiling((ny1 - ny0) * 0.5f * fb.Y);
|
||||
_gl.Enable(EnableCap.ScissorTest);
|
||||
_gl.Scissor(px, py, (uint)System.Math.Max(1, pw), (uint)System.Math.Max(1, ph));
|
||||
_gl.Clear(ClearBufferMask.DepthBufferBit);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
}
|
||||
```
|
||||
This requires `pvFrame` to be in scope. Currently `pvFrame` is declared inside the
|
||||
`if (clipRoot is not null)` block (`GameWindow.cs:7315`). Either hoist it or use
|
||||
`clipAssembly.HasOutsideView` (a property to add to `ClipFrameAssembly` if needed).
|
||||
**Confirm in Step 1:** check whether `ClipFrameAssembly` already exposes
|
||||
`OutsideView.Polygons.Count` or an equivalent. Add if absent.
|
||||
|
||||
Commit: `feat(render): Stage 4 — conditional Z-clear at doorway portal (retail DrawCells:432731)`
|
||||
|
||||
---
|
||||
|
||||
### T4.4 — Verify and document ceilings-are-capped (no code change expected)
|
||||
|
||||
**Retail:** Ceilings are capped BY CONSTRUCTION — each EnvCell's `drawing_bsp` is a
|
||||
closed mesh (floor, 4 walls, ceiling authored in the dat). There is no explicit "cap
|
||||
ceiling" step. `EnvCellRenderer.Render` draws each visible cell's mesh.
|
||||
|
||||
- [ ] **Confirm in Step 1:** Verify that `EnvCellRenderer.RegisterCell` stores the full
|
||||
cell mesh including ceiling polygons (not just floor + walls). The dat baking happens
|
||||
in `GameWindow.cs:5456–5502` (the `BuildLoadedCell`/`CellMesh.Build` call). If ceiling
|
||||
polygons are included in `CellMesh.Build`, the ceiling is capped. If they are
|
||||
explicitly excluded, add them.
|
||||
|
||||
- [ ] **Add a comment** in `EnvCellRenderer.Render` documenting that ceiling is present
|
||||
by construction (retail `DrawCells:432745`, `cell->structure->drawing_bsp` draws all
|
||||
cell surfaces including ceiling). No code change if already correct.
|
||||
|
||||
Commit (or annotation): `docs(render): Stage 4 — document ceiling sealed by EnvCell dat`
|
||||
|
||||
---
|
||||
|
||||
### T4.5 — Verify `EnvCellRenderer` GL_BLEND fix is complete and extends to the ceiling pass
|
||||
|
||||
**Current state (ALREADY FIXED):** `EnvCellRenderer.cs:1004–1023` already sets
|
||||
`_gl.Disable(EnableCap.Blend); _gl.DepthMask(true)` for the opaque pass and
|
||||
`_gl.Enable(EnableCap.Blend); _gl.DepthMask(false)` for the transparent pass. This fix
|
||||
is the U.4 root-cause fix for the transparent-walls regression (noted in the comment at
|
||||
`EnvCellRenderer.cs:1004`).
|
||||
|
||||
- [ ] **Verify the fix covers the call order after T4.1's sky-inside-clip-bracket
|
||||
change.** After T4.1, the order is: sky → terrain → `EnvCellRenderer.Render(Opaque)`
|
||||
→ entity dispatcher → `EnvCellRenderer.Render(Transparent)`. Sky may leave GL state
|
||||
dirty. The fix at line 1004 already re-establishes Blend + DepthMask at the TOP of
|
||||
`Render(…)`, so it is robust against any preceding state. Confirm no new state is
|
||||
needed (e.g. `DepthFunc`, `CullFace` — these are set at `GameWindow.cs:7030` before
|
||||
the clip bracket).
|
||||
|
||||
- [ ] **Add a `_gl.DepthMask(true)` reset at the END of `EnvCellRenderer.Render`** if
|
||||
the transparent pass leaves `DepthMask(false)`. Currently the code at line 1012 says
|
||||
"Restored to opaque defaults at the end of the draw loop" — verify this restoration
|
||||
actually exists (search for the matching `_gl.DepthMask(true)` after the draw loop in
|
||||
`EnvCellRenderer.cs:1025–1239`). If missing, add it before the method returns.
|
||||
|
||||
Commit: `fix(render): Stage 4 — verify EnvCellRenderer GL state restoration after transparent pass`
|
||||
|
||||
---
|
||||
|
||||
### T4.6 — Unit tests for PView BFS + OutsideView behavior
|
||||
|
||||
File: `tests/AcDream.Core.Tests/Rendering/PViewBfsTests.cs` (new; tests use
|
||||
`PortalVisibilityBuilder.Build` directly with synthetic `LoadedCell` fixtures).
|
||||
|
||||
- [ ] **Test: `OutsideView_NonEmpty_WhenExitPortalVisible`**
|
||||
Construct a `LoadedCell` with one portal whose `OtherCellId = 0xFFFF` (exit portal)
|
||||
and a non-degenerate polygon facing the camera. Call `PortalVisibilityBuilder.Build`
|
||||
from a camera position on the interior side. Assert `frame.OutsideView.Polygons.Count > 0`.
|
||||
|
||||
- [ ] **Test: `OutsideView_Empty_WhenNoExitPortal`**
|
||||
A cell with portals connecting to other interior cells only (`OtherCellId != 0xFFFF`).
|
||||
Assert `frame.OutsideView.Polygons.Count == 0`.
|
||||
|
||||
- [ ] **Test: `VisibleSet_ContainsRootCell_Always`**
|
||||
Any cell graph → root cell is always in `frame.OrderedVisibleCells` and is first.
|
||||
|
||||
- [ ] **Test: `VisibleSet_MultiCell_OrderedClosestFirst`**
|
||||
Root cell with portal to neighbour farther away → root appears at index 0.
|
||||
|
||||
- [ ] **Test: `BFS_Terminates_OnCyclicPortalGraph`**
|
||||
Root A→B, B→A (cycle). BFS must terminate with exactly 2 cells in `OrderedVisibleCells`
|
||||
(no infinite loop). This is the #95 dungeon-blowup guard.
|
||||
|
||||
Run: `dotnet test --filter "FullyQualifiedName~PViewBfsTests" -c Debug`.
|
||||
|
||||
Commit: `test(render): Stage 4 — PViewBfsTests (OutsideView + BFS termination)`
|
||||
|
||||
---
|
||||
|
||||
### Stage 4 visual gate
|
||||
|
||||
After T4.1–T4.6 green + `dotnet build` green:
|
||||
|
||||
> Ask the user to launch the client at Holtburg cottage:
|
||||
> - **Through the doorway from inside**: sky + rain visible in the doorway opening,
|
||||
> NOT full-screen. Outside terrain/buildings visible through the door.
|
||||
> - **No blue clear-color hole** in the doorway.
|
||||
> - **Ceiling is present** (no "no ceiling" regression).
|
||||
> - **Walls are opaque** (the transparent-walls regression is not re-introduced).
|
||||
> - **Dungeon** (if accessible): no terrain, no sky, walls/floors render — PVS BFS
|
||||
> converges without blowup.
|
||||
|
||||
---
|
||||
|
||||
## Stage 5 — Entity / particle cell clipping
|
||||
|
||||
**Goal:** Clip entities/particles to the PView visible cell set, not the world frustum.
|
||||
Kills NPC/door/smoke bleed-through.
|
||||
|
||||
**Retail anchor:**
|
||||
- `PView::DrawCells @ pseudo_c:432868–432882` — iterates `cell_draw_list` and for each
|
||||
calls `DrawObjCellForDummies(cell)`. Objects in a non-visible cell are never iterated.
|
||||
- `CObjCell::object_list` — objects live in their cell's object list (from `enter_cell`/
|
||||
`leave_cell`). Entity→cell membership comes from the physics shadow lists.
|
||||
|
||||
**Current state:** `WbDrawDispatcher.Draw` at `GameWindow.cs:7473–7476` already takes
|
||||
`visibleCellIds: visibility?.VisibleCellIds`. The `WbDrawDispatcher` uses this to gate
|
||||
entity draws (ParentCellId ∈ visibleCellIds). This is largely correct already. The
|
||||
gap: entities OUTSIDE a visible cell but INSIDE the world frustum may still draw.
|
||||
|
||||
### T5.1 — Verify entity-clip path is fully wired
|
||||
|
||||
- [ ] **Grep `WbDrawDispatcher.Draw` signature and its `visibleCellIds` parameter usage.**
|
||||
Find `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`, search for `visibleCellIds`.
|
||||
Confirm: when `visibleCellIds != null`, entities whose `ParentCellId` is NOT in the set
|
||||
are culled. If not: add the cull inside the dispatcher's entity loop.
|
||||
|
||||
- [ ] **Confirm `ParentCellId` is populated correctly for EnvCell-static objects.**
|
||||
Static objects inside a cell (inn furniture, door) have `ParentCellId` set to the
|
||||
cell's id. Verify at `GameWindow.BuildInteriorEntitiesForStreaming` or the matching
|
||||
`AddEntitiesToExistingLandblock` site that `ParentCellId` is set. If null or zero for
|
||||
static objects, the cell-clip gate misses them.
|
||||
|
||||
- [ ] **Use `OrderedVisibleCells` instead of `VisibleCellIds` for entity ordering.**
|
||||
`visibility.VisibleCellIds` is a `HashSet<uint>` (unordered). For strict retail-faithful
|
||||
entity draw ordering, the dispatcher should iterate in `frame.OrderedVisibleCells` order
|
||||
(closest first). This is an enhancement, not a blocker. Add a TODO comment if deferred.
|
||||
|
||||
Commit: `fix(render): Stage 5 — verify entity-clip visibleCellIds gate in WbDrawDispatcher`
|
||||
|
||||
---
|
||||
|
||||
### T5.2 — Particle cell clipping
|
||||
|
||||
**Current state:** `_particleRenderer.Draw(_particleSystem, camera, camPos,
|
||||
AcDream.Core.Vfx.ParticleRenderPass.Scene)` at `GameWindow.cs:7494–7496` does NOT take
|
||||
a `visibleCellIds` filter.
|
||||
|
||||
- [ ] **Check if `ParticleRenderer.Draw` has a cell-filter parameter.** If yes, pass
|
||||
`visibility?.VisibleCellIds`. If no, add it — particles in an invisible cell (NPC
|
||||
smoke from behind a sealed wall) should not draw.
|
||||
|
||||
- [ ] **Gate `SkyPreScene` and `SkyPostScene` particle passes by `renderSky`** (already
|
||||
gated at `GameWindow.cs:7506` for weather; confirm `SkyPreScene` at line 7272 is also
|
||||
gated by `renderSky`). This is the particle equivalent of Stage 3's sky gate.
|
||||
|
||||
Commit: `fix(render): Stage 5 — particle cell-clip via visibleCellIds`
|
||||
|
||||
---
|
||||
|
||||
### T5.3 — Unit tests for entity/particle cell-clip predicate
|
||||
|
||||
File: `tests/AcDream.Core.Tests/Rendering/EntityClipTests.cs` (new).
|
||||
|
||||
- [ ] **Test: `EntityClip_ParentInVisibleSet_IsIncluded`**
|
||||
Entity with `ParentCellId = 0xA9B40170` (the cottage cell); `visibleCellIds =
|
||||
{0xA9B40170}` → entity included.
|
||||
|
||||
- [ ] **Test: `EntityClip_ParentNotInVisibleSet_IsExcluded`**
|
||||
Entity with `ParentCellId = 0xA9B40172` (sealed room not in PVS); `visibleCellIds =
|
||||
{0xA9B40170}` → entity excluded.
|
||||
|
||||
- [ ] **Test: `EntityClip_NullVisibleSet_IncludesAll`**
|
||||
`visibleCellIds = null` (outdoor root) → all entities included (no gate).
|
||||
|
||||
Run: `dotnet test --filter "FullyQualifiedName~EntityClipTests" -c Debug`.
|
||||
|
||||
Commit: `test(render): Stage 5 — EntityClipTests`
|
||||
|
||||
---
|
||||
|
||||
### Stage 5 visual gate
|
||||
|
||||
> Ask the user to launch and walk to the Holtburg cottage door:
|
||||
> - **NPC just outside the door**: should be visible through the door when looking out,
|
||||
> but NOT visible through the wall from inside looking sideways (not through the portal
|
||||
> opening).
|
||||
> - **Smoke/particles from objects in invisible cells** do not bleed through walls.
|
||||
> - **No regression** on entity visibility in outdoor scenes (all entities visible when
|
||||
> `visibleCellIds` is null).
|
||||
|
||||
---
|
||||
|
||||
## Final acceptance — combined visual gate
|
||||
|
||||
After ALL stages green + full `dotnet test -c Debug` green:
|
||||
|
||||
> Ask the user to verify the full acceptance criteria (spec §6):
|
||||
>
|
||||
> **Cottage (Holtburg):**
|
||||
> - [ ] Walk through the doorway: no strobe (Stage 1–2, already done).
|
||||
> - [ ] Inside the cottage: interior sealed (walls, floor, ceiling all present).
|
||||
> - [ ] Looking at the door from inside: sky + rain visible through the doorway opening,
|
||||
> NOT full-screen. No blue clear-color hole.
|
||||
> - [ ] No transparent walls. No terrain bleeding through the floor.
|
||||
> - [ ] No entity/particle bleed through sealed walls.
|
||||
>
|
||||
> **Dungeon (if accessible):**
|
||||
> - [ ] No terrain, no sky (sealed dungeon).
|
||||
> - [ ] Walls/floors/ceilings render. Portal traversal converges (no FPS drop from BFS
|
||||
> blowup — issue #95 confirmed bounded by `seen.Add` gate).
|
||||
>
|
||||
> **`dotnet build` green.**
|
||||
> **`dotnet test -c Debug` — full suite green** (or the documented static-leak subset
|
||||
> fixed by T0.1; no new deterministic failures vs pre-Phase-W baseline).
|
||||
|
||||
---
|
||||
|
||||
## Roadmap update (implementer: do this in the same commit that clears the final visual gate)
|
||||
|
||||
- [ ] Update `docs/plans/2026-04-11-roadmap.md` — move Phase W to "shipped" with the
|
||||
commit SHA.
|
||||
- [ ] Update CLAUDE.md "Currently working toward" section to the next milestone.
|
||||
- [ ] Add a memory note to `memory/` if there are durable lessons (e.g. the sky-inside-
|
||||
clip-bracket pattern, the Z-clear for doorway depth).
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
|
||||
### Spec coverage
|
||||
|
||||
| Spec §2 item | Covered by |
|
||||
|---|---|
|
||||
| "Membership is transition-owned" | Stage 1–2 (already done/shipped) |
|
||||
| "Render roots at `CurrCell` + `seen_outside`" | T3.1, T3.2 |
|
||||
| "Camera offset via graph/BSP child lookup" | T3.3 |
|
||||
| "One PView portal traversal, `OutsideView`" | T4.1–T4.5 (PortalVisibilityBuilder already exists; Stage 4 wires the draw consequences) |
|
||||
| "`MaxReprocessPerCell` → `update_count` watermark" | Already done in `PortalVisibilityBuilder.cs:74–84` (`seen` HashSet = enqueue-once, equivalent to `cell_view_done`). No code change. |
|
||||
| "Draw landscape through exit portals; no blue hole" | T4.1 (sky inside clip), T4.2 (terrain viewpoint), T4.3 (Z-clear) |
|
||||
| "Cap ceilings" | T4.4 (verification only — already capped by construction) |
|
||||
| "`EnvCellRenderer` GL_BLEND fix" | T4.5 (already shipped in U.4; verify + extend) |
|
||||
| "Entity/particle clip to PView visible set" | T5.1, T5.2 |
|
||||
| "Dungeon: no terrain/sky" | T3.2 (`seen_outside=false → renderSky=false`), T4.6 test |
|
||||
|
||||
### Placeholder scan
|
||||
|
||||
All "Confirm in Step 1" items are explicitly labeled and have a concrete investigative
|
||||
action. They are marked as genuinely-uncertain integration points because the exact
|
||||
behavior depends on current shader and method bodies that would require reading additional
|
||||
files. They are NOT implementation-blocking — each has a fallback or a "no-op if already
|
||||
correct" path.
|
||||
|
||||
### Type consistency
|
||||
|
||||
- `physicsRoot` is `LoadedCell?` (render type) — correct; `CellGraph.CurrCell` is
|
||||
`ObjCell?` (Core type), and the conversion at `GameWindow.cs:7163–7165` (TryGetCell)
|
||||
bridges them. No new type conversions introduced.
|
||||
- `SeenOutside` on `LoadedCell` is `bool` (line 104 of `CellVisibility.cs`) — used as
|
||||
`physicsRoot?.SeenOutside ?? true` which is null-safe with the correct outdoor default.
|
||||
- `OutsideView.Polygons.Count` returns `int` (a `List<ViewPolygon>.Count`) — used in
|
||||
both `int > 0` checks and `ReadOnlySpan<Vector4>` plane extraction. No type mismatch.
|
||||
|
||||
### Stage 4 scope note
|
||||
|
||||
Stage 4 is large but all the underlying machinery (BFS, clip planes, terrain UBO,
|
||||
`ClipFrameAssembler`) is already in production. The stage is 5 concrete tasks (T4.1–T4.5)
|
||||
plus tests (T4.6). T4.1 (move sky inside clip bracket) is the most impactful; T4.3 (Z-
|
||||
clear) is the "no blue hole" fix; T4.4/T4.5 are verification. Estimated total: 2–3 hours
|
||||
for an implementer who has read this plan and the prerequisite files.
|
||||
|
||||
### Issue #102 note
|
||||
|
||||
Issue #102 (portal-graph BFS termination — too many reprocesses) is already closed by the
|
||||
`seen` HashSet in `PortalVisibilityBuilder.cs:84` (enqueue-once guarantee). No additional
|
||||
work needed for this plan. If the dungeon BFS still blows up in practice (Stage 4 visual
|
||||
gate), investigate whether `seen.Add(neighbourId)` is being bypassed; it should not be.
|
||||
Loading…
Add table
Add a link
Reference in a new issue