# Verbatim Retail Indoor Render Port — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the indoor-render approximation layer with a verbatim port of retail `PView::DrawCells` so interiors seal (no grey, no bleed, no half-character) and inside↔outside↔inside is seamless. **Architecture:** Keep the faithful membership (`PortalVisibilityBuilder` = `cell_draw_list`) and clip math (`PortalProjection.ProjectToClip`/`ClipToRegion` = `GetClip`/`polyClipFinish`). Rewrite `RetailPViewRenderer.DrawInside`/`DrawPortal` into retail's three `DrawCells` loops: draw **every** visible cell's shell, trimmed **per-slice** with `ClipPlaneSet`→`gl_ClipDistance`; draw objects membership+depth gated with **no** clip. Delete the `ClipFrameAssembler` slot-pool + `drawableCells` filter that drop shells (grey) and the global clip-off that caused bleed. **Tech Stack:** C# .NET 10, Silk.NET OpenGL 4.3 (bindless + MDI), xUnit. PowerShell on Windows; launch logs UTF-16. **Spec:** `docs/superpowers/specs/2026-06-06-verbatim-retail-indoor-render-port-design.md` (commit `eb7b1fa`). Read it first. **Worktree:** `thirsty-goldberg-51bb9b`, branch `claude/thirsty-goldberg-51bb9b`. Do NOT branch/worktree, push, or `git stash`/`gc`. Do NOT revert the dirty render tree. Build before launch. --- ## Orientation for the executing session (read once) - **Baseline:** App.Tests 205/205; Core.Tests 1331 pass / 4 fail (pre-existing Physics door/step-up, unrelated) / 1 skip; `dotnet build -c Debug` 0 errors. If these don't hold at start, stop and report — something drifted. - **The two bugs this plan kills** (both in `src/AcDream.App/Rendering/RetailPViewRenderer.cs`): 1. `:52` `drawableCells = clipAssembly.CellIdToSlot.Keys` + `if (!drawableCells.Contains(cellId)) continue;` in every loop → cells without a clip-slot are dropped → **grey**. 2. `:237` `UseIndoorMembershipOnlyRouting()` → `_envCells.SetClipRouting(null)` → trim globally off → **bleed**; it was disabled because the clip was (wrongly) applied to objects/characters → **half-character**. - **Retail oracle** (`docs/research/named-retail/acclient_2013_pseudo_c.txt`): `PView::DrawCells` 0x5a4840 — three loops over reverse `cell_draw_list`: exit-portal masks → shells (`DrawEnvCell`, per-slice `setup_view` clip) → objects (`DrawObjCellForDummies`, visibility-gated, NOT hard-clipped). - **Key existing APIs you'll reuse:** - `PortalVisibilityFrame` (in `PortalView.cs`): `OrderedVisibleCells` (List, closest-first), `CellViews` (Dictionary), `OutsideView` (CellView). `CellView.Polygons` is the slice list; `ViewPolygon.Vertices` is Vector2[] NDC. - `ClipPlaneSet.From(CellView)` → `Count`/`Planes` (≤8), `UseScissorFallback`+`ScissorNdcAabb`, `IsNothingVisible`. - `ClipFrame`: `Reset()`, `AppendSlot(ClipPlaneSet)`→slot index, `SetTerrainClip(planes)`, `UploadShared(gl)`, `RegionSsbo`. Slot 0 is reserved no-clip. - `EnvCellRenderer`: `SetClipRegionSsbo(uint)`, `SetClipRouting(IReadOnlyDictionary?)` (null = no-clip), `Render(WbRenderPass, HashSet filter)`, `PrepareRenderBatches(...)`. - `WbDrawDispatcher`: `Draw(camera, entries, frustum, neverCullLandblockId, visibleCellIds, animatedEntityIds)`, `SetClipRegionSsbo`, `ClearClipRouting()`. - **Verification reality:** the pure draw-ORDER is unit-tested (Task 1). Every GL task is verified by **launch + your eyes** + the `[render-sig]` / `[shell]` probes — there is no unit test for a GPU draw. Launch command is in the Appendix. **Do not mark a GL task done on a green build alone — only on the user's visual confirmation.** --- ## File Structure - **Create:** `src/AcDream.App/Rendering/IndoorDrawPlan.cs` — pure (GL-free) function turning a `PortalVisibilityFrame` into the reverse-ordered shell draw list (every visible cell, no filter). Test seam for the grey regression. - **Create:** `tests/AcDream.App.Tests/Rendering/IndoorDrawPlanTests.cs`. - **Rewrite:** `src/AcDream.App/Rendering/RetailPViewRenderer.cs` — the three `DrawCells` loops; remove `drawableCells`, `UseIndoorMembershipOnlyRouting`, the `ClipFrameAssembler` dependency. - **Delete:** `src/AcDream.App/Rendering/ClipFrameAssembler.cs` + `tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs` (and the `ClipViewSlice`/`ClipFrameAssembly` types if unused after the rewrite). - **Light touch:** `src/AcDream.App/Rendering/GameWindow.cs` — the indoor/look-in call sites + `sig*` diagnostics that read `ClipAssembly.OutsideViewSlices` (switch to `pvFrame.OutsideView.Polygons.Count`). - **Untouched:** `PortalVisibilityBuilder.cs`, `PortalProjection.cs`, `ClipPlaneSet.cs`, `ClipFrame.cs`, `EnvCellRenderer.cs`, `WbDrawDispatcher.cs`, the outdoor `LScape` branch. --- ## Task 1: Pure shell-draw-order function (pins the grey regression) **Files:** - Create: `src/AcDream.App/Rendering/IndoorDrawPlan.cs` - Test: `tests/AcDream.App.Tests/Rendering/IndoorDrawPlanTests.cs` - [ ] **Step 1: Write the failing test** ```csharp using System.Collections.Generic; using System.Linq; using System.Numerics; using AcDream.App.Rendering; using Xunit; namespace AcDream.App.Tests.Rendering; public class IndoorDrawPlanTests { private static ViewPolygon Quad() => new(new[] { new Vector2(-1,-1), new Vector2(1,-1), new Vector2(1,1), new Vector2(-1,1) }); private static PortalVisibilityFrame FrameWith(params uint[] orderedCells) { var f = new PortalVisibilityFrame(); foreach (var id in orderedCells) { f.OrderedVisibleCells.Add(id); var v = new CellView(); v.Add(Quad()); f.CellViews[id] = v; } return f; } [Fact] public void ShellPass_IncludesEveryVisibleCell_NoFilter() { // The grey bug: a cell in OrderedVisibleCells must NEVER be dropped from the // shell pass. (Old code dropped cells lacking a ClipFrameAssembler slot.) var f = FrameWith(0x01, 0x02, 0x03); var plan = IndoorDrawPlan.ShellPass(f); Assert.Equal(new uint[] { 0x03, 0x02, 0x01 }, plan.Select(e => e.CellId).ToArray()); // reverse = far→near Assert.All(plan, e => Assert.NotEmpty(e.Slices)); } [Fact] public void ShellPass_ExcludesEmptyViewCells() { var f = FrameWith(0x01); f.OrderedVisibleCells.Add(0x02); // present in the list… f.CellViews[0x02] = new CellView(); // …but empty view → not drawable var plan = IndoorDrawPlan.ShellPass(f); Assert.Equal(new uint[] { 0x01 }, plan.Select(e => e.CellId).ToArray()); } } ``` - [ ] **Step 2: Run test to verify it fails** Run: `dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~IndoorDrawPlanTests"` Expected: FAIL — `IndoorDrawPlan` does not exist (compile error). - [ ] **Step 3: Write minimal implementation** ```csharp // IndoorDrawPlan.cs // // Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840): // the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a // non-empty view is included — there is NO "drawable" filter. Dropping cells without a // clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed). using System.Collections.Generic; namespace AcDream.App.Rendering; public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices); public static class IndoorDrawPlan { /// Reverse OrderedVisibleCells (far→near), each visible cell with its view /// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped /// (they are not actually visible); no other cell is ever dropped. public static List ShellPass(PortalVisibilityFrame frame) { var result = new List(frame.OrderedVisibleCells.Count); for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--) { uint cellId = frame.OrderedVisibleCells[i]; if (!frame.CellViews.TryGetValue(cellId, out var view) || view.IsEmpty) continue; result.Add(new CellDrawEntry(cellId, view.Polygons)); } return result; } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~IndoorDrawPlanTests"` Expected: PASS (2 tests). - [ ] **Step 5: Commit** ```powershell git add src/AcDream.App/Rendering/IndoorDrawPlan.cs tests/AcDream.App.Tests/Rendering/IndoorDrawPlanTests.cs git commit -m @' feat(render): IndoorDrawPlan.ShellPass — every visible cell, no drawable filter (R1) Pure port of retail DrawCells membership: reverse cell_draw_list, per-slice. Pins the grey regression — a cell in OrderedVisibleCells is never dropped from the shell pass. Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 2: Shell pass draws EVERY visible cell (kill the grey) — highest-value, smallest change This is the change that should make the grey disappear. Keep the existing per-slice clip routing for now (Task 4 cleans it); only remove the `drawableCells` filter and iterate `IndoorDrawPlan.ShellPass`. **Files:** Modify `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawEnvCellShells`, ~`:175-197`). - [ ] **Step 1: Replace the body of `DrawEnvCellShells`** Replace the loop so it iterates `IndoorDrawPlan.ShellPass(pvFrame)` (every visible cell) instead of `pvFrame.OrderedVisibleCells` gated by `drawableCells.Contains`. Keep `GetCellSlicesOrNoClip` + `UseShellClipRouting` exactly as they are for this task. New body: ```csharp private void DrawEnvCellShells( IRetailPViewCellDrawCallbacks ctx, PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells) // param kept this task; removed in Task 4 { foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) { uint cellId = entry.CellId; _oneCell.Clear(); _oneCell.Add(cellId); foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) { UseShellClipRouting(cellId, slice); _envCells.Render(WbRenderPass.Opaque, _oneCell); _envCells.Render(WbRenderPass.Transparent, _oneCell); } } } ``` Note: `GetCellSlicesOrNoClip` already returns `NoClipSlice` for a cell with no assembler slice, so a cell that used to be dropped now draws **unclipped** — sealed (grey gone), possibly with some bleed (Task 4 fixes the trim). - [ ] **Step 2: Build** Run: `dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --nologo` Expected: `Build succeeded. 0 Error(s)`. - [ ] **Step 3: Run the App test suite (no regression)** Run: `dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --no-build` Expected: 207/207 pass (205 baseline + 2 new). - [ ] **Step 4: Launch + VISUAL verify (user)** Launch with probes (Appendix). Stand inside the cottage + go to the cellar. **Acceptance:** the interior walls/floor no longer render grey — every room you're in is sealed. Some bleed through doorways is expected at this step. In the log, `[render-sig] draw=[…]` should list the same cells as `ids=[…]` (no cell dropped). **Do not proceed until the user confirms the grey is gone.** - [ ] **Step 5: Commit** ```powershell git add src/AcDream.App/Rendering/RetailPViewRenderer.cs git commit -m @' fix(render): shell pass draws every visible cell — kill the grey (R1) Iterate IndoorDrawPlan.ShellPass (all visible cells) instead of gating on the drawableCells slot filter. A visible cell whose shell was dropped for lack of a ClipFrameAssembler slot now draws, sealing the interior. Per-slice trim unchanged this commit (Task 4 replaces it). Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 3: Object pass draws every visible cell's objects, no clip (kill the half-character) **Files:** Modify `RetailPViewRenderer.cs` (`DrawCellObjectLists`, ~`:199-224`). - [ ] **Step 1: Replace the loop to use `IndoorDrawPlan.ShellPass` order + ensure no clip on objects** ```csharp private void DrawCellObjectLists( IRetailPViewCellDrawContext ctx, PortalVisibilityFrame pvFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells, // kept this task; removed in Task 4 InteriorEntityPartition.Result partition) { foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) { uint cellId = entry.CellId; if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0) continue; _oneCell.Clear(); _oneCell.Add(cellId); UseIndoorMembershipOnlyRouting(); // objects: NO clip planes (retail DrawObjCellForDummies) DrawEntityBucket(ctx, bucket, _oneCell); foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, bucket)); } } ``` (Functionally close to today, but iterating the full visible set. `UseIndoorMembershipOnlyRouting` stays here on purpose — objects are never clip-planed. It is removed from the shell path in Task 4.) - [ ] **Step 2: Build + App tests** Run: `dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --nologo` then `dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --no-build` Expected: build 0 errors; 207/207. - [ ] **Step 3: Launch + VISUAL verify (user)** Stand on the cellar stairs / near a door with an NPC. **Acceptance:** characters/objects are **whole** (no half-character), and objects in visible cells appear. Confirm with the user before proceeding. - [ ] **Step 4: Commit** ```powershell git add src/AcDream.App/Rendering/RetailPViewRenderer.cs git commit -m @' fix(render): object pass over every visible cell, no clip planes (R1) Objects drawn membership+depth gated (retail DrawObjCellForDummies), never hard-clipped to the 2D portal view — fixes the half-character. Iterates the full visible-cell set. Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 4: Per-slice shell trim from `ClipPlaneSet.From` directly (drop the ClipFrameAssembler dependency in the shell loop) Now make the shell trim faithful and self-contained: compute each slice's planes from the cell's own `CellView.Polygons` via `ClipPlaneSet.From`, pack one region into `ClipFrame`, route the cell to it. This removes the shell loop's use of `clipAssembly` and `drawableCells`. **Files:** Modify `RetailPViewRenderer.cs` (`DrawEnvCellShells`, helpers). - [ ] **Step 1: Add a per-slice shell-clip helper** ```csharp // Pack ONE slice's convex region into the clip frame (slot 1) and route this cell to it, // matching retail setup_view(cell, slice). Slot 0 stays no-clip. Returns false when the // slice is empty (draw nothing) — the caller skips it. private bool ApplyShellSliceClip(uint cellId, ViewPolygon slice, Action setTerrainClipUbo) { var oneSlice = new CellView(); oneSlice.Add(slice); var planes = ClipPlaneSet.From(oneSlice); if (planes.IsNothingVisible) return false; _clipFrame.Reset(); // slot 0 = no-clip int slot = _clipFrame.AppendSlot(planes); // slot 1 = this slice (or no-clip region if scissor fallback*) UploadClipFrame(setTerrainClipUbo); // re-upload SSBO (cheap; few indoor cells) _oneCellSlot.Clear(); _oneCellSlot[cellId] = slot; _envCells.SetClipRouting(_oneCellSlot); _entities.ClearClipRouting(); return true; } ``` \* `ClipFrame.AppendSlot(ClipPlaneSet)` packs the planes when `Count>0`, else a no-clip region. The scissor-AABB fallback (`UseScissorFallback`) is not expressible as planes — for this first port, treat scissor-fallback as no-clip (over-include, never grey). A later refinement can `glScissor` the `ScissorNdcAabb`; note it in the handoff if you see bleed only on >8-edge slices. - [ ] **Step 2: Rewrite `DrawEnvCellShells` to use it (and drop `clipAssembly`/`drawableCells` params)** ```csharp private void DrawEnvCellShells( RetailPViewDrawContext ctx, // need SetTerrainClipUbo; use the concrete context PortalVisibilityFrame pvFrame) { foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame)) { _oneCell.Clear(); _oneCell.Add(entry.CellId); foreach (var slice in entry.Slices) { if (!ApplyShellSliceClip(entry.CellId, slice, ctx.SetTerrainClipUbo)) continue; _envCells.Render(WbRenderPass.Opaque, _oneCell); _envCells.Render(WbRenderPass.Transparent, _oneCell); } } } ``` (For `DrawPortal`, which uses `RetailPViewPortalDrawContext`, either add `SetTerrainClipUbo` access via the shared `IRetailPViewCellDrawContext` interface or pass the `Action` directly. Both contexts already expose `SetTerrainClipUbo`.) Update the two call sites (`DrawInside` `:77`, `DrawPortal` `:126`) to `DrawEnvCellShells(ctx, pvFrame)`. - [ ] **Step 3: Build + App tests** Run: build + `dotnet test … --no-build`. Expected: 0 errors; 207/207 (the deleted clip-slot routing isn't unit-tested). - [ ] **Step 4: Launch + VISUAL verify (user)** **Acceptance:** interior is sealed AND trimmed — looking through a doorway shows only the slice of the next room visible through the opening; no bleed of a neighbour room past a wall edge; no shell gap at stair/door boundaries. If a shell gaps, the slice is too small (a `ClipToRegion` case to inspect) — report it; do not re-add the filter. Confirm with the user. - [ ] **Step 5: Commit** ```powershell git add src/AcDream.App/Rendering/RetailPViewRenderer.cs git commit -m @' feat(render): per-slice shell trim from ClipPlaneSet.From (retail setup_view) (R1) Each visible cell's shell is clipped per portal_view slice via ClipPlaneSet→gl_ClipDistance, computed from the cell's own CellView — no ClipFrameAssembler slot pool, no drawableCells filter. Objects remain unclipped. Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 5: Look-out (landscape through OutsideView) from `OutsideView` slices directly Drop the shell/object loops' last `clipAssembly` use and make the landscape pass read `pvFrame.OutsideView` directly. **Files:** Modify `RetailPViewRenderer.cs` (`DrawLandscapeThroughOutsideView` `:133-153`, `DrawInside` `:74`). - [ ] **Step 1: Rewrite `DrawLandscapeThroughOutsideView` to iterate `pvFrame.OutsideView.Polygons`** ```csharp private void DrawLandscapeThroughOutsideView( RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame, InteriorEntityPartition.Result partition) { if (pvFrame.OutsideView.IsEmpty) return; foreach (var slicePoly in pvFrame.OutsideView.Polygons) { var oneSlice = new CellView(); oneSlice.Add(slicePoly); var planes = ClipPlaneSet.From(oneSlice); if (planes.IsNothingVisible) continue; _clipFrame.SetTerrainClip(planes.Count > 0 ? ToSpan(planes) : ReadOnlySpan.Empty); UploadClipFrame(ctx.SetTerrainClipUbo); // Reuse the existing ClipViewSlice DTO ONLY as the landscape callback payload, or // introduce a small RetailPViewLandscapeSliceContext from (planes, slicePoly bounds). ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(/* slice payload */, partition.Outdoor)); } // depth-clear over the OutsideView bounds (retail Clear(DEPTH) when outside_view.view_count>0) ctx.ClearDepthSlice?.Invoke(/* OutsideView NDC bounds */); } private static ReadOnlySpan ToSpan(ClipPlaneSet s) { var a = new Vector4[s.Count]; for (int i = 0; i < s.Count; i++) a[i] = s.Planes[i]; return a; } ``` Note: `RetailPViewLandscapeSliceContext` / `ClearDepthSlice` currently take a `ClipViewSlice`. Simplest path: keep a tiny `ClipViewSlice` record (just `{ Planes, NdcAabb }`) as a payload DTO even after deleting `ClipFrameAssembler`, OR change those two callback signatures to take `(ReadOnlySpan planes, Vector4 ndcAabb)`. Pick one and apply consistently to the `GameWindow` callback implementations (`DrawRetailPViewLandscapeSlice`, the `ClearDepthSlice` lambda at `GameWindow.cs:7480`). - [ ] **Step 2: Update `DrawInside` to call the new signatures and drop `clipAssembly`/`drawableCells`** In `DrawInside` (`:39-81`): remove `var clipAssembly = ClipFrameAssembler.Assemble(...)`, `drawableCells`, and the `UseIndoorMembershipOnlyRouting()` calls around the shell loop. Partition with **all** visible cells: ```csharp var partition = InteriorEntityPartition.Partition( new HashSet(pvFrame.OrderedVisibleCells), ctx.LandblockEntries); ``` Final `DrawInside` order: `Build` → `_envCells.PrepareRenderBatches(filter: visibleSet)` → `DrawLandscapeThroughOutsideView(ctx, pvFrame, partition)` → `DrawExitPortalMasks` (if kept) → `DrawEnvCellShells(ctx, pvFrame)` → `DrawCellObjectLists(ctx, pvFrame, partition)`. - [ ] **Step 3: Build + App tests + Launch VISUAL verify (user)** Build 0 errors; 207/207. **Acceptance:** standing inside, looking out a door/window shows the outdoor world through the opening (not grey, not full-screen); depth correct. Confirm with the user. - [ ] **Step 4: Commit** ```powershell git add src/AcDream.App/Rendering/RetailPViewRenderer.cs src/AcDream.App/Rendering/GameWindow.cs git commit -m @' feat(render): look-out landscape from OutsideView slices; drop clipAssembly in DrawInside (R1) DrawInside no longer builds a ClipFrameAssembler; landscape-through-outside_view reads pvFrame.OutsideView directly per slice. Partition runs over all visible cells. Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 6: Delete `ClipFrameAssembler` + the slot-pool dead code After Tasks 4–5 nothing should reference `ClipFrameAssembler` / `ClipFrameAssembly` / `drawableCells` in the draw path. **Files:** Delete `ClipFrameAssembler.cs`, `ClipFrameAssemblerTests.cs`; edit `RetailPViewRenderer.cs`, `GameWindow.cs`. - [ ] **Step 1: Remove remaining references** - `RetailPViewRenderer.cs`: remove `using`/fields/params for `ClipFrameAssembly`, `drawableCells`, `_oneCellSlot` if now unused, `GetCellSlicesOrNoClip`, `UseShellClipRouting`, and `RetailPViewFrameResult.ClipAssembly`/`DrawableCells` (keep `PortalFrame`, `Partition`). Update `DrawExitPortalMasks` to iterate `IndoorDrawPlan.ShellPass` + `entry.Slices` (or delete it if exit masks are not needed — see note). - `GameWindow.cs`: `sigTerrainDrawn`/`sigSkyDrawn`/`sigDepthClear`/`sigSceneParticles` (`:7506-7514`) currently read `pviewResult.ClipAssembly.OutsideViewSlices.Length`; change to `pviewResult.PortalFrame.OutsideView.Polygons.Count`. Remove other `ClipAssembly`/`DrawableCells` reads. **Exit-masks note:** retail Loop 1 (`DrawPortalPolyInternal`) punches exit-portal openings into depth for the look-out landscape. If, after Task 5, the look-out landscape shows correctly without it, delete `DrawExitPortalMasks`. If the outdoor world z-fights or is occluded wrong through the opening, keep it and port it as: per visible cell, per slice, draw the cell's exit-portal polygons depth-only. Decide from the Task 5 visual. - [ ] **Step 2: Delete the files** ```powershell git rm src/AcDream.App/Rendering/ClipFrameAssembler.cs tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs ``` If `ClipViewSlice`/`ClipFrameAssembly` live in `ClipFrameAssembler.cs` and a payload DTO is still needed (Task 5), move the minimal record to a new small file `RetailPViewTypes.cs` (or inline into `RetailPViewRenderer.cs`). - [ ] **Step 3: Build + FULL test suite** Run: `dotnet build -c Debug --nologo` then `dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --no-build` and `dotnet test tests\AcDream.Core.Tests\AcDream.Core.Tests.csproj -c Debug --no-build`. Expected: build 0 errors; App green (now ~205–207 depending on removed assembler tests); Core 1331/4(pre-existing)/1. - [ ] **Step 4: Commit** ```powershell git add -A git commit -m @' refactor(render): delete ClipFrameAssembler slot-pool + drawableCells filter (R1) The verbatim DrawCells loop (Tasks 1–5) no longer needs the slot pool or the drawable-cells filter that dropped shells. Removes the approximation layer. Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 7: Look-in (B) — `DrawPortal` on the same loops `DrawPortal` already calls `DrawEnvCellShells`/`DrawCellObjectLists`; after Tasks 4–6 those are filter-free and clip-correct, so look-in should "just work." This task verifies it and removes any residual `clipAssembly` use in `DrawPortal`. **Files:** Modify `RetailPViewRenderer.cs` (`DrawPortal` `:83-131`). - [ ] **Step 1: Mirror DrawInside's cleanup in DrawPortal** Remove `ClipFrameAssembler.Assemble`, `drawableCells`; partition over `pvFrame.OrderedVisibleCells`; call `DrawEnvCellShells(ctx, pvFrame)` + `DrawCellObjectLists(ctx, pvFrame, partition)`; keep the `RestoreNoClip` at the end. - [ ] **Step 2: Build + App tests** Build 0 errors; App green. - [ ] **Step 3: Launch + VISUAL verify (user)** Walk up to a building from OUTSIDE with an open door. **Acceptance:** you see the room interior (shell + objects) through the doorway as you approach, sealed and trimmed; walking through the door is seamless (no flash/grey at the threshold). Confirm with the user. - [ ] **Step 4: Commit** ```powershell git add src/AcDream.App/Rendering/RetailPViewRenderer.cs git commit -m @' feat(render): look-in DrawPortal on the verbatim DrawCells loops (R1, scope B) Outside-looking-in reuses the same per-cell, per-slice shell + object passes as DrawInside; no separate engine, no slot pool. Co-Authored-By: Claude Opus 4.8 (1M context) '@ ``` --- ## Task 8: Final verification + handoff - [ ] **Step 1: Full green** `dotnet build -c Debug --nologo`; App + Core test suites. Record counts. - [ ] **Step 2: Clean launch (no probes) for FPS/feel + the four seamless scenarios** With the user: (1) stand inside cottage — sealed, textured-or-grey noted; (2) cellar — floor + stairs present, character whole; (3) room↔room↔cellar transitions — no flap; (4) walk outside→in and look in through a door — seamless. Get explicit visual sign-off on each. - [ ] **Step 3: If walls are sealed but UNTEXTURED (grey-but-drawn)** That is the separate surface/texture bug the spec flags as out of scope (HEAD's "interior walls grey"). File it as a new issue in `docs/ISSUES.md` with the evidence (every shell now draws per `[render-sig]`, but surfaces render untextured) — do NOT reopen the membership/clip work for it. - [ ] **Step 4: Update memory + roadmap** If the seal holds and the user confirms: add a `memory/` entry (e.g. `feedback_verbatim_drawcells_port.md`) capturing that the grey was the `drawableCells` filter + the verbatim `DrawCells` loop (every cell, per-slice shell clip, objects unclipped) fixed it; update `MEMORY.md`. Note the two-handoff contradiction (2026-06-05 shell-sealing vs 2026-06-06 projection) so it isn't re-litigated. --- ## Self-Review (done while writing — recorded for the executor) - **Spec coverage:** §4.2 loop → Tasks 2/3/4 (shells/objects/trim); §4.3 look-in → Task 7; §4 look-out → Task 5; §4.1 deletions → Task 6; testing §7 → Task 1 (pure pin) + per-task visual gates; risks §8 → Task 4 Step 4 note + Task 8 Step 3. Covered. - **Type consistency:** `IndoorDrawPlan.ShellPass` → `List`; `CellDrawEntry(uint CellId, IReadOnlyList Slices)`; `ClipPlaneSet.From(CellView)`; `ClipFrame.Reset()/AppendSlot/SetTerrainClip/UploadShared`; `EnvCellRenderer.SetClipRouting/Render`. Consistent across tasks. - **Known soft spots (flagged inline, not placeholders):** (a) the landscape/depth-clear callback DTO (`ClipViewSlice` vs a new `(planes, aabb)`) — Task 5 Step 1 picks one explicitly; (b) exit-masks keep-or-delete — Task 6 Step 1 decides from the Task 5 visual; (c) scissor-AABB fallback rendered as no-clip for now — Task 4 note. These are genuine implementation choices for the executor, each with a stated default. --- ## Appendix: launch command (probes) ```powershell Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | ForEach-Object { $_.CloseMainWindow() | Out-Null; if (-not $_.WaitForExit(5000)) { $_ | 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-drawcells.log" ``` Run in the background; the user tests then closes the window. For a clean FPS/feel run, drop the three `ACDREAM_PROBE_*` vars. **Build before every launch.** Heavy probe output can make the client sluggish — keep probe runs short.