acdream/docs/superpowers/plans/2026-06-06-verbatim-retail-indoor-render-port.md
Erik 2ec8f41200 docs: implementation plan + pickup handoff — verbatim retail DrawCells port
Task-by-task plan (TDD pin for the grey regression + per-task visual gates) to replace the
indoor-render approximation with a verbatim PView::DrawCells port, sequenced so Task 2 alone
should kill the grey. Pickup handoff for a fresh session: state, baselines, rules, do-not-relitigate.
Local commit only (not pushed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:44:19 +02:00

29 KiB
Raw Permalink Blame History

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 ClipPlaneSetgl_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<uint, CellView>), 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<uint,int>?) (null = no-clip), Render(WbRenderPass, HashSet<uint> 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

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
// 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<ViewPolygon> Slices);

public static class IndoorDrawPlan
{
    /// <summary>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.</summary>
    public static List<CellDrawEntry> ShellPass(PortalVisibilityFrame frame)
    {
        var result = new List<CellDrawEntry>(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
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) <noreply@anthropic.com>
'@

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:

private void DrawEnvCellShells(
    IRetailPViewCellDrawCallbacks ctx,
    PortalVisibilityFrame pvFrame,
    ClipFrameAssembly clipAssembly,
    HashSet<uint> 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
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) <noreply@anthropic.com>
'@

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
private void DrawCellObjectLists(
    IRetailPViewCellDrawContext ctx,
    PortalVisibilityFrame pvFrame,
    ClipFrameAssembly clipAssembly,
    HashSet<uint> 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
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) <noreply@anthropic.com>
'@

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
// 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<uint> 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)
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<uint> 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
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) <noreply@anthropic.com>
'@

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
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<Vector4>.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<Vector4> 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<Vector4> 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:

var partition = InteriorEntityPartition.Partition(
    new HashSet<uint>(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
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) <noreply@anthropic.com>
'@

Task 6: Delete ClipFrameAssembler + the slot-pool dead code

After Tasks 45 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

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 ~205207 depending on removed assembler tests); Core 1331/4(pre-existing)/1.

  • Step 4: Commit
git add -A
git commit -m @'
refactor(render): delete ClipFrameAssembler slot-pool + drawableCells filter (R1)

The verbatim DrawCells loop (Tasks 15) 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) <noreply@anthropic.com>
'@

Task 7: Look-in (B) — DrawPortal on the same loops

DrawPortal already calls DrawEnvCellShells/DrawCellObjectLists; after Tasks 46 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
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) <noreply@anthropic.com>
'@

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.ShellPassList<CellDrawEntry>; CellDrawEntry(uint CellId, IReadOnlyList<ViewPolygon> 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)

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.