feat(render): R1 — binary render decision, indoor = per-cell DrawInside only

GameWindow.OnRender: when clipRoot != null, run only InteriorRenderer.DrawInside
(per-cell shells + per-cell objects + live-dynamics); the global entity pass +
global shell pass are no longer issued indoors. Outdoor scenery drawn clipped to
the doorway (after terrain, before the Z-clear). Outdoor root path unchanged.
pvFrame hoisted so the splice reads OrderedVisibleCells; per-frame 3-bucket
partition built on the indoor root. Retail RenderNormalMode @ 0x453aa0.

InteriorRenderer amended with a DrawableCells membership filter (an IsNothingVisible
cell can be in OrderedVisibleCells but absent from CellIdToSlot — iterate for ORDER,
filter for membership; matches the old envCellShellFilter set exactly).

Build green, 174/174 App tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 20:01:53 +02:00
parent 4b75c68ea3
commit c4fd71149a
2 changed files with 72 additions and 26 deletions

View file

@ -11,6 +11,12 @@ public sealed class InteriorRenderContext
/// <summary>Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame.</summary>
public required IReadOnlyList<uint> OrderedVisibleCells { get; init; }
/// <summary>The cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys =
/// the GameWindow envCellShellFilter). A cell may appear in <see cref="OrderedVisibleCells"/> but
/// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the
/// membership filter; <see cref="OrderedVisibleCells"/> supplies the draw ORDER.</summary>
public required IReadOnlySet<uint> DrawableCells { get; init; }
/// <summary>The 3-bucket entity split (<see cref="InteriorEntityPartition"/>). Only ByCell +
/// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door
/// step (clipped to OutsideView).</summary>
@ -54,6 +60,7 @@ public sealed class InteriorRenderer
// Loop A — per-cell OPAQUE shell + that cell's static objects (closest-first).
foreach (uint cellId in ctx.OrderedVisibleCells)
{
if (!ctx.DrawableCells.Contains(cellId)) continue; // no clip slot ⇒ assembler culled it
_oneCell.Clear();
_oneCell.Add(cellId);
_envCells.Render(WbRenderPass.Opaque, _oneCell);
@ -70,6 +77,7 @@ public sealed class InteriorRenderer
// Loop B — per-cell TRANSPARENT shells (stained glass / additive cell surfaces).
foreach (uint cellId in ctx.OrderedVisibleCells)
{
if (!ctx.DrawableCells.Contains(cellId)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
_envCells.Render(WbRenderPass.Transparent, _oneCell);