fix(render): FW4 slice 1 - interior outside-view slices come from the walk

The FW3 visual gate's stairwell/grass transition flash (grass briefly
covering floor openings at doorway crossings - the #119 family) was the
FW3 dual path leaking: the walk decided WHETHER terrain draws while the
old PortalVisibilityBuilder assembly decided WHERE (slice planes, count,
scissor), and punch fans indexed the old slice array with walk view
indices. The new ACDREAM_PROBE_WALK_ROOT apparatus pinned the boundary
frames: fat/degenerate old-apparatus exit views splash terrain over
interior pixels, the interior depth-clear preserves color, and cells
absent from the walk's flood never repaint. Retail has ONE visibility
structure and cannot produce this.

ClipFrameAssembler.ReassembleOutsideViewFromWalk now materializes the
walk's own outside_view (pixel screen points -> standard NDC -> the
existing ClipPlaneSet machinery) into the assembly's outside-view block
after Collect, ahead of the single PrepareClipFrame publication (moved
below the walk block). The Landscape event carries the walk's active
view count on the record's existing OutsideViewCount field (trace
mapping compares kind only - zero oracle-fixture churn) and the driver
fans exactly that many terrain slices; activeTerrainSliceCount is
deleted end to end. Outdoor roots keep the assembler's single
full-screen slice, asserted ==1.

Hermetic 6,762/0 (4 new materializer tests pin the y-flip and
plane-sign conventions), Walk lane 209/1, InstalledDat walk conformance
40/1. Seals/cell slices/look-in seeding stay on the old per-cell views
for the rest of FW4 (identical dat polygons; only the visible set can
differ).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 18:21:08 +02:00
parent 8f8b0b9c57
commit 37febd1fe6
11 changed files with 468 additions and 44 deletions

View file

@ -315,6 +315,7 @@ issue is closed, the strip was missed; delete both.
| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` | | `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` |
| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` | | `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
| `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` | | `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` |
| `ACDREAM_PROBE_WALK_ROOT` | FW3 visual-gate apparatus (stairwell-transition defect) — dies with that fix | `=1` | after every interior/outdoor root flip, prints 8 `[walk-root]` lines (one per frame): viewer cell, root cell, camera-cell resolution, slice count, eye position, and the walk's visited cell list — pins which crossing frames root wrong or flood short | print-only | `RenderingDiagnostics.ProbeWalkRootEnabled` |
| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` | | `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` |
| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B | | `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B |
| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` | | `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` |

View file

@ -475,6 +475,37 @@ hygiene).
**Goal:** everything draws at its walk turn; the patch apparatus and the **Goal:** everything draws at its walk turn; the patch apparatus and the
old visibility layer are gone. old visibility layer are gone.
**FW4 SLICE 1 — the interior outside-view source cutover (2026-08-30,
pulled forward from the FW3 visual gate).** The owner's stairwell/grass
transition report (grass briefly covering floor openings at doorway
crossings, intermittent — the #119 family) probe-pinned to the FW3 dual
path: the walk decided WHETHER terrain draws (`_interiorPView.OutsideView.
ViewCount > 0`) while the old `PortalVisibilityBuilder` assembly decided
WHERE (slice clip planes + count + scissor), and the walk's punch fans
indexed the OLD slice array with WALK view indices. At boundary frames
(`ACDREAM_PROBE_WALK_ROOT` captures: the slices=2 short-flood family at
OUT→IN, a 1-frame outdoor blip mid-crossing) the old builder's exit views
went fat/degenerate, terrain splashed over interior pixels, and the
depth-clear preserves COLOR — any cell absent from the walk's flood never
repainted its pixels. Retail cannot produce this: it has ONE visibility
structure. The cutover: `ClipFrameAssembler.ReassembleOutsideViewFromWalk`
materializes the walk's own `outside_view` (pixel points → standard NDC →
the existing `ClipPlaneSet.From` machinery) into the assembly's
outside-view block right after Collect, before the single
`PrepareClipFrame` publication (moved below the walk block); the
`Landscape` event now carries the walk's active-view count (rides the
record's existing `OutsideViewCount` field — trace mapping compares kind
only, ZERO oracle-fixture churn) and the driver fans exactly that many
terrain slices — `activeTerrainSliceCount` is deleted end to end. Outdoor
roots keep the assembler's single full-screen slice (asserted ==1;
identical content by construction). Suites: hermetic 6,762/0 (4 new
materializer tests pinning the y-flip + plane-sign conventions), Walk
lane 209/1, InstalledDat walk conformance 40/1 — all unchanged or grown.
Remaining desync surfaces deliberately left for the rest of FW4: exit
SEALS + cell slices still assemble from the old per-cell views (identical
dat portal polygons; only the visible SET can differ), and interior-root
look-in seeding still reads the old outside view.
- Entities gate per view via `Render::viewconeCheck` at their cell's walk - Entities gate per view via `Render::viewconeCheck` at their cell's walk
turn; dynamics-last and alpha interleave per the walk's stages turn; dynamics-last and alpha interleave per the walk's stages
(`RetailAlphaQueue` becomes the stream's alpha stage or is absorbed — (`RetailAlphaQueue` becomes the stream's alpha stage or is absorbed —

View file

@ -144,6 +144,18 @@ public sealed class ClipFrameAssembly
internal void SetOutsideViewSlices(ClipViewSlice[] slices) => OutsideViewSlices = slices; internal void SetOutsideViewSlices(ClipViewSlice[] slices) => OutsideViewSlices = slices;
/// <summary>FW4 slice 1: returns the outside-view slice array to the
/// pool ahead of a same-frame reassembly from the walk's own views
/// (<see cref="ClipFrameAssembler.ReassembleOutsideViewFromWalk"/>) —
/// without this the replaced array would leak from the slice pool for
/// the frame.</summary>
internal void ReturnOutsideViewSlicesForReassembly()
{
if (OutsideViewSlices.Length != 0)
ReturnSlices(OutsideViewSlices);
OutsideViewSlices = System.Array.Empty<ClipViewSlice>();
}
private ClipViewSlice[] RentSlices(int length) private ClipViewSlice[] RentSlices(int length)
{ {
if (_sliceArraysByLength.TryGetValue(length, out Stack<ClipViewSlice[]>? pool) if (_sliceArraysByLength.TryGetValue(length, out Stack<ClipViewSlice[]>? pool)
@ -380,6 +392,143 @@ public static class ClipFrameAssembler
return assembly; return assembly;
} }
/// <summary>
/// Campaign FW4 slice 1 — the interior root's outside-view cutover.
/// Replaces the assembly's outside-view block (slices, terrain mode,
/// scissor/NDC bounds, plane count) with slices derived from THE WALK'S
/// OWN <c>outside_view</c> (<see cref="Walk.RetailFrameWalk.InteriorOutsideView"/>),
/// filled by the walk's <c>ConstructView</c> during Collect. Retail has
/// exactly ONE visibility structure per frame: <c>LScape::draw</c>'s
/// terrain clip, the punch fans' <c>building_view</c> planes, and the
/// landscape turn's view count all read the views the walk itself
/// installed. Feeding these from the old <c>PortalVisibilityBuilder</c>
/// assembly let the two systems desynchronize on camera-transition
/// boundary frames — terrain splashed through stale/fat old-apparatus
/// exit views over interior pixels the walk's flood never repainted
/// (the FW3 visual-gate stairwell/grass flash, probe-pinned
/// 2026-08-30), and punch fans indexed the old slice array with walk
/// view indices.
///
/// The walk stores view vertices as PIXEL screen points
/// (<c>copy_view</c>'s post-divide viewport coordinates, origin
/// top-left, +Y down — <see cref="Walk.WalkScreenClip.TransformToScreen"/>:
/// x=(W/2)(x_c+w), y=(H/2)(wy_c)); inverting that mapping yields the
/// standard NDC this assembler's <see cref="ViewPolygon"/>s already use
/// (ndcX = 2x/W 1, ndcY = 1 2y/H). Winding is normalized inside
/// <see cref="ClipPlaneSet.From(in ViewPolygon)"/>, and the closing
/// duplicate vertex <c>copy_view</c> stores is merged there too.
///
/// Must run AFTER the walk's Collect and BEFORE
/// <c>PrepareClipFrame</c> publishes the clip regions — appended slots
/// join the same single publication.
/// </summary>
public static void ReassembleOutsideViewFromWalk(
ClipFrameAssembly assembly,
Walk.WalkPortalView outsideView,
float viewportWidth,
float viewportHeight)
{
System.ArgumentNullException.ThrowIfNull(assembly);
System.ArgumentNullException.ThrowIfNull(outsideView);
if (viewportWidth <= 0f || viewportHeight <= 0f)
{
throw new System.ArgumentOutOfRangeException(
nameof(viewportWidth),
$"viewport {viewportWidth}x{viewportHeight} — the walk projected its "
+ "views through a real viewport; a non-positive extent here means the "
+ "caller handed a different frame's context (fail-loud rule).");
}
ClipFrame frame = assembly.Frame;
int viewCount = outsideView.ViewCount;
var polys = outsideView.View.Polys;
var pool = outsideView.View.Vertices;
if (polys.Count < viewCount)
{
throw new System.InvalidOperationException(
$"walk outside_view holds {polys.Count} polys for ViewCount={viewCount} — "
+ "the view set's append bookkeeping desynchronized (fail-loud rule).");
}
assembly.ReturnOutsideViewSlicesForReassembly();
List<ClipViewSlice> outsideSlicesList = assembly.SliceScratch;
outsideSlicesList.Clear();
int outsideMaxPlaneCount = 0;
bool outsideHasScissorFallback = false;
int scissorFallbacks = assembly.ScissorFallbacks;
float unionMinX = float.MaxValue, unionMinY = float.MaxValue;
float unionMaxX = float.MinValue, unionMaxY = float.MinValue;
for (int v = 0; v < viewCount; v++)
{
Walk.WalkViewPoly walkPoly = polys[v];
var vertices = new Vector2[walkPoly.VertexCount];
for (int k = 0; k < walkPoly.VertexCount; k++)
{
Vector2 px = pool[walkPoly.VertexIndex + k].Point;
vertices[k] = new Vector2(
px.X / viewportWidth * 2f - 1f,
1f - px.Y / viewportHeight * 2f);
}
var poly = new ViewPolygon(vertices);
if (!poly.IsEmpty)
{
if (poly.MinX < unionMinX) unionMinX = poly.MinX;
if (poly.MinY < unionMinY) unionMinY = poly.MinY;
if (poly.MaxX > unionMaxX) unionMaxX = poly.MaxX;
if (poly.MaxY > unionMaxY) unionMaxY = poly.MaxY;
}
var cps = ClipPlaneSet.From(poly);
if (cps.IsNothingVisible)
continue;
int slot;
Vector4[] planes;
if (cps.Count > 0)
{
planes = cps.PlaneArray;
slot = frame.AppendSlot(planes);
if (cps.Count > outsideMaxPlaneCount)
outsideMaxPlaneCount = cps.Count;
}
else
{
planes = System.Array.Empty<Vector4>();
slot = 0;
outsideHasScissorFallback = true;
scissorFallbacks++;
}
outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
}
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
bool outdoorVisible = outsideViewSlices.Length > 0;
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
TerrainClipMode terrainMode = !outdoorVisible
? TerrainClipMode.Skip
: (outsideHasScissorFallback ? TerrainClipMode.Scissor : TerrainClipMode.Planes);
Vector4 outsideViewNdcAabb = outdoorVisible
? new Vector4(unionMinX, unionMinY, unionMaxX, unionMaxY)
: Vector4.Zero;
Vector4 terrainScissor = terrainMode == TerrainClipMode.Scissor
? outsideViewNdcAabb
: Vector4.Zero;
assembly.SetOutsideViewSlices(outsideViewSlices);
assembly.OutdoorSlot = outdoorSlot;
assembly.OutdoorVisible = outdoorVisible;
assembly.TerrainMode = terrainMode;
assembly.TerrainScissorNdcAabb = terrainScissor;
assembly.HasOutsideView = outdoorVisible;
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
assembly.OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0;
assembly.ScissorFallbacks = scissorFallbacks;
}
/// <summary> /// <summary>
/// Appends the cell views used by nested <c>DrawBuilding -&gt; DrawPortal</c> /// Appends the cell views used by nested <c>DrawBuilding -&gt; DrawPortal</c>
/// PViews to the already assembled frame. Retail installs each nested /// PViews to the already assembled frame. Retail installs each nested

View file

@ -100,6 +100,13 @@ public sealed class RetailPViewRenderer
// cleared in finally. // cleared in finally.
private Action? _walkPreClearDynamics; private Action? _walkPreClearDynamics;
// ACDREAM_PROBE_WALK_ROOT (FW3 visual-gate apparatus, throwaway): the
// previous frame's root kind + a post-flip frame countdown so each
// interior/outdoor transition dumps 8 frames of rooting facts.
private bool? _probeWalkRootPrevOutdoor;
private int _probeWalkRootFramesLeft;
private ulong _probeWalkRootFrame;
// Campaign FW3.2b-2: the walk's production world-data registries // Campaign FW3.2b-2: the walk's production world-data registries
// (published/retired by LandblockRenderPublisher) plus the per-frame // (published/retired by LandblockRenderPublisher) plus the per-frame
// driver state. Null until the composition passes them; the static // driver state. Null until the composition passes them; the static
@ -189,8 +196,10 @@ public sealed class RetailPViewRenderer
pvFrame, pvFrame,
_clipAssemblyScratch); _clipAssemblyScratch);
passes.AppendLookInClipFrames(_lookInFrames, clipAssembly); passes.AppendLookInClipFrames(_lookInFrames, clipAssembly);
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2); // FW4 slice 1: PrepareClipFrame (the one clip-region publication)
passes.PrepareClipFrame(terrainUploadCount); // moved BELOW the walk block — an interior-rooted walk frame
// re-derives the outside-view slices from the walk's own views
// first, so the appended slots join the same single publication.
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the // R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition, // assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
@ -288,15 +297,15 @@ public sealed class RetailPViewRenderer
} }
} }
int activeTerrainSliceCount = clipAssembly.OutsideViewSlices.Length; if (ctx.RootCell.IsOutdoorNode && clipAssembly.OutsideViewSlices.Length != 1)
if (ctx.RootCell.IsOutdoorNode && activeTerrainSliceCount != 1)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"walk static cutover: an outdoor root's clip assembly produced " "walk static cutover: an outdoor root's clip assembly produced "
+ $"{activeTerrainSliceCount} outside-view slices, not the expected 1 — " + $"{clipAssembly.OutsideViewSlices.Length} outside-view slices, not the "
+ "WalkFrameDriver's landscape turn assumes the outdoor root's default " + "expected 1 — the outdoor root draws through the full-screen default "
+ "full-screen view (plan §FW3 item 2c's pinned assumption; assert " + "view (retail set_default_view; the walk fans exactly its own 1), and "
+ "rather than silently coercing to 1)."); + "the outdoor slice data still comes from the assembler (plan §FW3 item "
+ "2c's pinned assumption; assert rather than silently coercing to 1).");
} }
_walkWorldData!.BeginFrame( _walkWorldData!.BeginFrame(
@ -333,9 +342,60 @@ public sealed class RetailPViewRenderer
walkDriver.Collect( walkDriver.Collect(
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext, _frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
ctx.ViewProjection, ctx.CameraWorldPosition, activeTerrainSliceCount); ctx.ViewProjection, ctx.CameraWorldPosition);
// FW4 slice 1: an interior root's terrain/sky/punch clip slices
// come from THE WALK'S OWN outside_view — retail's one
// visibility structure. See ClipFrameAssembler.
// ReassembleOutsideViewFromWalk's doc comment for the boundary-
// frame desync (the stairwell/grass flash) this retires. The
// outdoor root keeps the assembler's single full-screen slice
// (asserted ==1 above; identical content by construction).
if (walkCameraCell is not null)
{
ClipFrameAssembler.ReassembleOutsideViewFromWalk(
clipAssembly,
_frameWalk.InteriorOutsideView,
viewportWidth,
viewportHeight);
} }
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
_probeWalkRootFrame++;
bool outdoorNow = ctx.RootCell.IsOutdoorNode;
if (_probeWalkRootPrevOutdoor is bool prev && prev != outdoorNow)
{
_probeWalkRootFramesLeft = 8;
Console.WriteLine(
$"[walk-root] ---- FLIP {(prev ? "OUT->IN" : "IN->OUT")} at frame {_probeWalkRootFrame} ----");
}
_probeWalkRootPrevOutdoor = outdoorNow;
if (_probeWalkRootFramesLeft > 0)
{
_probeWalkRootFramesLeft--;
var cellsList = new List<uint>(walkDriver.VisitedCells);
cellsList.Sort();
string cells = string.Join(",", cellsList.ConvertAll(c => c.ToString("x8"))
.GetRange(0, Math.Min(cellsList.Count, 10)));
Console.WriteLine(
$"[walk-root] f={_probeWalkRootFrame} out={(outdoorNow ? 1 : 0)} "
+ $"viewer=0x{ctx.ViewerCellId:X8} root=0x{ctx.RootCell.CellId:X8} "
+ $"res={ctx.CameraCellResolution} slices={clipAssembly.OutsideViewSlices.Length} "
+ $"eye=({ctx.ViewerEyePos.X:F2},{ctx.ViewerEyePos.Y:F2},{ctx.ViewerEyePos.Z:F2}) "
+ $"walkCells={walkDriver.VisitedCells.Count} bld={walkDriver.VisitedBuildings.Count} "
+ $"cells=[{cells}]");
}
}
}
// FW4 slice 1: the ONE clip-region publication, after any walk
// reassembly so the walk-derived outside-view slots are included
// (moved from directly after AssembleClipFrame; the count is
// reservation metadata the RHI arm ignores).
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
passes.PrepareClipFrame(terrainUploadCount);
// #124: look-in cells need prepared shell batches + their statics routed // #124: look-in cells need prepared shell batches + their statics routed
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never // cell-object pass iterates pvFrame.OrderedVisibleCells, which never

View file

@ -68,6 +68,16 @@ public sealed class RetailFrameWalk
/// under the same conditions).</summary> /// under the same conditions).</summary>
public float DegradeMultiplier = WalkBuilding.DefaultDegradeMultiplier; public float DegradeMultiplier = WalkBuilding.DefaultDegradeMultiplier;
/// <summary>FW4 slice 1: the interior root's <c>outside_view</c> — the
/// exit-view polygons THIS walk's own <c>ConstructView</c> installed
/// during the most recent <see cref="DrawInside"/>. The production
/// renderer materializes the frame's terrain/sky/punch clip slices from
/// these views (retail has exactly one visibility structure; the old
/// PortalVisibilityBuilder assembly no longer feeds the walk path's
/// outside views). Valid after <see cref="WalkFrame"/> returns for an
/// interior root, until the next interior root's view push resets it.</summary>
internal WalkPortalView InteriorOutsideView => _interiorPView.OutsideView;
/// <summary>The per-frame root (<c>SmartBox::RenderNormalMode</c>). /// <summary>The per-frame root (<c>SmartBox::RenderNormalMode</c>).
/// <paramref name="cameraCell"/> may be null only when the camera is /// <paramref name="cameraCell"/> may be null only when the camera is
/// outdoors.</summary> /// outdoors.</summary>
@ -137,7 +147,7 @@ public sealed class RetailFrameWalk
WalkLandscape landscape, WalkPortalView activeViews, WalkLandscape landscape, WalkPortalView activeViews,
IRetailFrameWalkContext ctx, IWalkEventSink sink) IRetailFrameWalkContext ctx, IWalkEventSink sink)
{ {
sink.Emit(WalkEvent.Landscape()); sink.Emit(WalkEvent.Landscape(activeViews.ViewCount));
landscape.CalcDrawOrder(); landscape.CalcDrawOrder();
landscape.CheckBlocks(ctx.CyPlane, activeViews); landscape.CheckBlocks(ctx.CyPlane, activeViews);

View file

@ -35,8 +35,17 @@ public readonly record struct WalkEvent(
int OutsideViewCount, int OutsideViewCount,
IReadOnlyList<uint> Cells) IReadOnlyList<uint> Cells)
{ {
public static WalkEvent Landscape() /// <summary>FW4 slice 1: the landscape turn carries the ACTIVE view
=> new(WalkEventKind.Landscape, 0, 0, Array.Empty<uint>()); /// set's <c>ViewCount</c> (retail <c>LScape::draw</c> iterates the
/// views the walk itself installed — 1 for the outdoor root's
/// full-screen default view, the interior root's own surviving
/// exit-view count otherwise). The driver fans exactly this many
/// terrain slices; the old clip apparatus no longer supplies the
/// count. Reuses the record's existing <see cref="WalkEvent.OutsideViewCount"/>
/// field; the conformance trace mapping compares the kind only
/// (<c>LS</c>), so oracle fixtures are unaffected.</summary>
public static WalkEvent Landscape(int activeViewCount)
=> new(WalkEventKind.Landscape, 0, activeViewCount, Array.Empty<uint>());
public static WalkEvent Building(uint positionCellId) public static WalkEvent Building(uint positionCellId)
=> new(WalkEventKind.Building, positionCellId, 0, Array.Empty<uint>()); => new(WalkEventKind.Building, positionCellId, 0, Array.Empty<uint>());

View file

@ -395,7 +395,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink
private IWalkBuildingFrameContext? _ctx; private IWalkBuildingFrameContext? _ctx;
private Matrix4x4 _viewProjection; private Matrix4x4 _viewProjection;
private Vector3 _cameraWorldPosition; private Vector3 _cameraWorldPosition;
private int _activeTerrainSliceCount;
private bool _skyDrawnThisFrame; private bool _skyDrawnThisFrame;
private WalkDrawStage? _currentDcStage; private WalkDrawStage? _currentDcStage;
private bool _readyToReplay; private bool _readyToReplay;
@ -429,15 +428,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink
IGpuFrame frame, IGpuFrame frame,
IGpuPassEncoder encoder, IGpuPassEncoder encoder,
Matrix4x4 viewProjection, Matrix4x4 viewProjection,
Vector3 cameraWorldPosition, Vector3 cameraWorldPosition)
int activeTerrainSliceCount = 1)
{ {
ArgumentNullException.ThrowIfNull(frame); ArgumentNullException.ThrowIfNull(frame);
ArgumentNullException.ThrowIfNull(encoder); ArgumentNullException.ThrowIfNull(encoder);
Collect( Collect(
walk, cameraCellId, cameraCell, landscape, ctx, walk, cameraCellId, cameraCell, landscape, ctx,
viewProjection, cameraWorldPosition, activeTerrainSliceCount); viewProjection, cameraWorldPosition);
Replay(frame, encoder); Replay(frame, encoder);
} }
@ -454,14 +452,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink
WalkLandscape landscape, WalkLandscape landscape,
IRetailFrameWalkContext ctx, IRetailFrameWalkContext ctx,
Matrix4x4 viewProjection, Matrix4x4 viewProjection,
Vector3 cameraWorldPosition, Vector3 cameraWorldPosition)
int activeTerrainSliceCount)
{ {
ArgumentNullException.ThrowIfNull(walk); ArgumentNullException.ThrowIfNull(walk);
ArgumentNullException.ThrowIfNull(landscape); ArgumentNullException.ThrowIfNull(landscape);
ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(ctx);
BeginFrame(ctx, viewProjection, cameraWorldPosition, activeTerrainSliceCount); BeginFrame(ctx, viewProjection, cameraWorldPosition);
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this); walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
EndFrame(); EndFrame();
} }
@ -478,16 +475,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink
internal void BeginFrame( internal void BeginFrame(
IWalkBuildingFrameContext ctx, IWalkBuildingFrameContext ctx,
Matrix4x4 viewProjection, Matrix4x4 viewProjection,
Vector3 cameraWorldPosition, Vector3 cameraWorldPosition)
int activeTerrainSliceCount)
{ {
ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(ctx);
if (activeTerrainSliceCount < 0)
{
throw new ArgumentOutOfRangeException(
nameof(activeTerrainSliceCount), activeTerrainSliceCount,
"The active terrain slice count cannot be negative.");
}
if (_ctx is not null) if (_ctx is not null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@ -499,7 +489,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink
_ctx = ctx; _ctx = ctx;
_viewProjection = viewProjection; _viewProjection = viewProjection;
_cameraWorldPosition = cameraWorldPosition; _cameraWorldPosition = cameraWorldPosition;
_activeTerrainSliceCount = activeTerrainSliceCount;
_skyDrawnThisFrame = false; _skyDrawnThisFrame = false;
_currentDcStage = null; _currentDcStage = null;
_readyToReplay = false; _readyToReplay = false;
@ -616,7 +605,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
VisitedCells.Add(walkEvent.CellId); VisitedCells.Add(walkEvent.CellId);
break; break;
case WalkEventKind.Landscape: case WalkEventKind.Landscape:
HandleLandscapeTurn(); HandleLandscapeTurn(walkEvent.OutsideViewCount);
break; break;
case WalkEventKind.DrawCells: case WalkEventKind.DrawCells:
foreach (uint id in walkEvent.Cells) foreach (uint id in walkEvent.Cells)
@ -710,9 +699,18 @@ internal sealed class WalkFrameDriver : IWalkEventSink
// Turn handlers // Turn handlers
// ------------------------------------------------------------------ // ------------------------------------------------------------------
private void HandleLandscapeTurn() private void HandleLandscapeTurn(int activeViewCount)
{ {
RequireOpenFrame(); RequireOpenFrame();
if (activeViewCount < 1)
{
throw new InvalidOperationException(
$"A Landscape turn fired with {activeViewCount} active views — "
+ "RetailFrameWalk only draws the landscape through an installed view set "
+ "(the outdoor root's full-screen default view, or an interior root's "
+ "surviving exit views, both at least 1). A zero/negative count is a "
+ "walk/driver desync (Campaign FW fail-loud rule).");
}
if (_skyDrawnThisFrame) if (_skyDrawnThisFrame)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@ -727,7 +725,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink
MarkIfGrown(); MarkIfGrown();
_events.Add(WalkFrameEvent.Sky()); _events.Add(WalkFrameEvent.Sky());
_skyDrawnThisFrame = true; _skyDrawnThisFrame = true;
for (int slice = 0; slice < _activeTerrainSliceCount; slice++) // FW4 slice 1: the fan count is the WALK'S OWN active view count
// (carried on the Landscape event) — retail LScape::draw runs once
// per view the walk installed. The old clip apparatus's slice count
// no longer participates; the renderer re-derives the actual slice
// clip data from the same walk views before replay, so index i here
// and OutsideViewSlices[i] there are the same view by construction.
for (int slice = 0; slice < activeViewCount; slice++)
_events.Add(WalkFrameEvent.TerrainSlice(slice)); _events.Add(WalkFrameEvent.TerrainSlice(slice));
} }

View file

@ -171,6 +171,17 @@ public static class RenderingDiagnostics
public static bool ProbeWalkShadowEnabled { get; set; } = public static bool ProbeWalkShadowEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_WALK_SHADOW") == "1"; Environment.GetEnvironmentVariable("ACDREAM_PROBE_WALK_SHADOW") == "1";
/// <summary>
/// FW3 visual-gate apparatus (2026-08-30, throwaway — dies with the
/// stairwell-transition fix): when true, RetailPViewRenderer prints one
/// <c>[walk-root]</c> line per frame for the 8 frames after every
/// interior/outdoor ROOT flip — viewer cell, resolution, slice count,
/// and the walk's visited cell list — pinning which frames draw grass
/// over floor openings during a doorway crossing and why.
/// </summary>
public static bool ProbeWalkRootEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_WALK_ROOT") == "1";
/// <summary> /// <summary>
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits /// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues, /// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,

View file

@ -24,8 +24,12 @@ public sealed class RetailPViewPassExecutorTests
"begin", "begin",
"assemble", "assemble",
"append-look-in-clips", "append-look-in-clips",
"prepare-clip:3", // FW4 slice 1: the clip-region publication moved below the
// walk block (a walk-rooted interior frame re-derives the
// outside-view slices from the walk's own views first, and
// the appended slots must join the same single publication).
"indoor-routing", "indoor-routing",
"prepare-clip:3",
"prepare-cells", "prepare-cells",
"diagnostics", "diagnostics",
"terrain-clip", "terrain-clip",

View file

@ -233,8 +233,7 @@ public sealed class WalkFrameDriverTests
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.RunFrame( driver.RunFrame(
walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: landscape, walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: landscape,
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero, ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
activeTerrainSliceCount: 1);
Assert.Equal( Assert.Equal(
new[] new[]
@ -306,8 +305,7 @@ public sealed class WalkFrameDriverTests
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.RunFrame( driver.RunFrame(
walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: new WalkLandscape(), walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: new WalkLandscape(),
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero, ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
activeTerrainSliceCount: 0);
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape // No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
// at all — but CLEAR/SEALS still fire unconditionally. // at all — but CLEAR/SEALS still fire unconditionally.
@ -401,7 +399,7 @@ public sealed class WalkFrameDriverTests
Assert.Equal(1, activeView.ViewCount); Assert.Equal(1, activeView.ViewCount);
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0); driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
walk.DrawBuilding(building, activeView, ctx, driver); walk.DrawBuilding(building, activeView, ctx, driver);
driver.EndFrame(); driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass); driver.Replay(draw.Frame, draw.Pass);
@ -434,7 +432,7 @@ public sealed class WalkFrameDriverTests
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData()); var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0); driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
Assert.Throws<InvalidOperationException>( Assert.Throws<InvalidOperationException>(
() => ((IWalkEventSink)driver).Emit(WalkEvent.DrawCells(0, [0x100u]))); () => ((IWalkEventSink)driver).Emit(WalkEvent.DrawCells(0, [0x100u])));
@ -451,15 +449,15 @@ public sealed class WalkFrameDriverTests
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData()); var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0); driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
Assert.Throws<InvalidOperationException>( Assert.Throws<InvalidOperationException>(
() => driver.BeginFrame( () => driver.BeginFrame(
ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0)); ctx, Matrix4x4.Identity, Vector3.Zero));
driver.EndFrame(); driver.EndFrame();
// EndFrame cleared the open-frame guard: BeginFrame is usable again. // EndFrame cleared the open-frame guard: BeginFrame is usable again.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0); driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
driver.EndFrame(); driver.EndFrame();
} }
@ -488,7 +486,7 @@ public sealed class WalkFrameDriverTests
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData()); var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0); driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
Assert.Throws<InvalidOperationException>(() => driver.Replay(draw.Frame, draw.Pass)); Assert.Throws<InvalidOperationException>(() => driver.Replay(draw.Frame, draw.Pass));
} }
@ -516,7 +514,7 @@ public sealed class WalkFrameDriverTests
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.Collect( driver.Collect(
walk, cameraCellId: 0u, cameraCell: null, landscape, ctx, walk, cameraCellId: 0u, cameraCell: null, landscape, ctx,
Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero, activeTerrainSliceCount: 1); Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
// No GPU calls at all yet — Collect is CPU-only. // No GPU calls at all yet — Collect is CPU-only.
Assert.Empty(log); Assert.Empty(log);
@ -549,7 +547,7 @@ public sealed class WalkFrameDriverTests
fx.Dispatcher, new RecordingLeafRenderer(log), worldData, new RecordingTrace(log)); fx.Dispatcher, new RecordingLeafRenderer(log), worldData, new RecordingTrace(log));
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0); driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
((IWalkEventSink)driver).OnLandscapeCellTurn(0x8C040005u); ((IWalkEventSink)driver).OnLandscapeCellTurn(0x8C040005u);
Assert.Empty(log); // no GPU work at Collect time; nothing recorded to the log yet Assert.Empty(log); // no GPU work at Collect time; nothing recorded to the log yet
driver.EndFrame(); driver.EndFrame();

View file

@ -0,0 +1,147 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Walk;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Campaign FW4 slice 1 — <see cref="ClipFrameAssembler.ReassembleOutsideViewFromWalk"/>:
/// an interior root's outside-view slices come from the walk's OWN
/// <c>outside_view</c> (pixel screen points, origin top-left, +Y down),
/// converted to the assembler's standard-NDC <see cref="ViewPolygon"/>s.
/// </summary>
public class WalkOutsideViewReassemblyTests
{
private const float W = 1024f, H = 720f;
private sealed class StubRays : IWalkRayCaster
{
public Vector3 RayThrough(float screenX, float screenY) =>
Vector3.Normalize(new Vector3(screenX - W / 2f, screenY - H / 2f, 1000f));
}
private static WalkPortalView WalkViewOfPixelQuads(params Vector2[][] pixelQuads)
{
var view = new WalkPortalView();
var rays = new StubRays();
foreach (Vector2[] quad in pixelQuads)
{
var pts = new WalkScreenPoint[quad.Length];
for (int i = 0; i < quad.Length; i++)
pts[i] = new WalkScreenPoint(quad[i].X, quad[i].Y, 0f, 1f);
Assert.True(WalkCopyView.Append(view, pts, rays, Vector3.Zero));
}
return view;
}
private static ClipFrameAssembly AssembledWithOldOutside()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(new ViewPolygon(new[]
{
new Vector2(-0.9f, -0.9f), new Vector2(0.9f, -0.9f),
new Vector2(0.9f, 0.9f), new Vector2(-0.9f, 0.9f),
}));
return ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
}
[Fact]
public void PixelDoorway_MapsToExpectedNdcAabbAndPlanes()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
// Pixel quad x∈[256,512], y∈[180,360] → NDC x∈[-0.5,0], y∈[0,0.5]
// (yNdc = 1 2·py/H flips the axis: py=180 → +0.5, py=360 → 0).
WalkPortalView walkView = WalkViewOfPixelQuads(new[]
{
new Vector2(256f, 180f), new Vector2(512f, 180f),
new Vector2(512f, 360f), new Vector2(256f, 360f),
});
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
Assert.True(asm.OutdoorVisible);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(slice.Slot, asm.OutdoorSlot);
Assert.NotEqual(0, slice.Slot);
Assert.Equal(-0.5f, slice.NdcAabb.X, 3);
Assert.Equal(0f, slice.NdcAabb.Y, 3);
Assert.Equal(0f, slice.NdcAabb.Z, 3);
Assert.Equal(0.5f, slice.NdcAabb.W, 3);
Assert.Equal(slice.NdcAabb, asm.OutsideViewNdcAabb);
// Every corner of the doorway satisfies every inward plane
// (n·p + d >= 0), and a point far outside fails at least one.
Vector2[] ndcCorners =
{
new(-0.5f, 0f), new(0f, 0f), new(0f, 0.5f), new(-0.5f, 0.5f),
};
foreach (Vector2 corner in ndcCorners)
{
foreach (Vector4 plane in slice.Planes)
Assert.True(plane.X * corner.X + plane.Y * corner.Y + plane.W >= -1e-4f);
}
bool outsideFails = false;
foreach (Vector4 plane in slice.Planes)
outsideFails |= plane.X * 0.9f + plane.Y * -0.9f + plane.W < 0f;
Assert.True(outsideFails);
}
[Fact]
public void FullViewportWalkQuad_CoversFullNdc()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
var walkView = new WalkPortalView();
Assert.True(WalkCopyView.AppendFullViewportQuad(
walkView, new StubRays(), Vector3.Zero, W, H));
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
Assert.Equal(new Vector4(-1f, -1f, 1f, 1f), slice.NdcAabb);
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
}
[Fact]
public void EmptyWalkView_YieldsSkipMode()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
Assert.True(asm.OutdoorVisible); // the old view was visible pre-cutover
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, new WalkPortalView(), W, H);
Assert.Empty(asm.OutsideViewSlices);
Assert.False(asm.OutdoorVisible);
Assert.False(asm.HasOutsideView);
Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode);
Assert.Equal(0, asm.OutsidePlaneCount);
Assert.Equal(0, asm.OutdoorSlot);
}
[Fact]
public void TwoWalkViews_ProduceTwoSlicesWithDistinctSlots()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
WalkPortalView walkView = WalkViewOfPixelQuads(
new[]
{
new Vector2(100f, 100f), new Vector2(300f, 100f),
new Vector2(300f, 300f), new Vector2(100f, 300f),
},
new[]
{
new Vector2(600f, 400f), new Vector2(900f, 400f),
new Vector2(900f, 650f), new Vector2(600f, 650f),
});
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
Assert.Equal(2, asm.OutsideViewSlices.Length);
Assert.NotEqual(asm.OutsideViewSlices[0].Slot, asm.OutsideViewSlices[1].Slot);
// The union AABB spans both doorways.
Assert.True(asm.OutsideViewNdcAabb.X < asm.OutsideViewSlices[0].NdcAabb.Z);
Assert.True(asm.OutsideViewNdcAabb.Z >= asm.OutsideViewSlices[1].NdcAabb.X);
}
}