Round-1 three-lens FAIL at d60ca4ea0 found: the K2 fix was right in
production but nothing pinned the draw side (restoring the pre-fix gate
left every lane green); a stale "still ends an active scissor" comment
asserted a mechanism the same round had already deleted; the sky.vert
comment claimed retail clips the sky when it draws unclipped; the K6
pin ran through ClipFrameAssembler.Assemble, which has zero production
callers, so it proved nothing about the producer that actually runs.
This round closes L1-L9.
L1 (BLOCKING) — draw-side weather-gate pin. Added
RetailPViewPassExecutorTests.DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired:
reads DrawLandscapeDynamicsPhase's compiled call graph and asserts (a)
the call immediately before DrawWeatherOnce is
WalkFrameDriver.get_WeatherTurnFired and (b) exactly one
brfalse/brfalse.s branch sits between that call and the draw, jumping
forward past it. Three mutations, each shown failing:
M1 (restore `if (clipAssembly.OutsideViewSlices.Length != 0)`):
Assert.Equal() Failure: Values differ
Expected: typeof(AcDream.App.Rendering.Walk.WalkFrameDriver)
Actual: typeof(AcDream.App.Rendering.ClipFrameAssembly)
M2 (drop the gate, unconditional call):
Assert.Equal() Failure: Values differ
Expected: typeof(AcDream.App.Rendering.Walk.WalkFrameDriver)
Actual: typeof(AcDream.App.Rendering.RetailPViewPassExecutor)
M3 (invert to `if (!walkDriver.WeatherTurnFired)`):
Assert.Single() Failure: The collection did not contain any matching items
Expected: (predicate expression)
Collection: [CompiledBranch { Offset = 7, OpCode = brfalse.s, TargetOffset = 17 }, CompiledBranch { Offset = 24, OpCode = brtrue.s, TargetOffset = 33 }]
Corrected WalkFrameDriverTranscriptTests.cs's doc comment (the
Collect_WeatherTurnFiredMatchesThePrintedOcLineExactly block): it pins
the print half and the flag only, and now names the real draw-side pin
instead of falsely claiming to double as one.
L2 (MAJOR) — scissor stack deleted, K4 comment corrected. Deleted
IWorldPassSurface.BeginScissor/EndScissor and their RhiWorldPassSurface
bodies, the scissor call inside ClearInteriorDepth, NdcScissorRect.cs +
NdcScissorRectTests.cs (BeginScissor had zero remaining callers, and
RhiWorldPassSurface.BeginScissor was NdcScissorRect.ToPixels' only
production caller). This left RhiWorldPassSurface's 4th constructor
parameter (IRetailPViewFramebufferSource) and RetailPViewPassExecutor.cs's
RetailPViewFramebufferSize/IRetailPViewFramebufferSource/
SilkRetailPViewFramebufferSource types entirely dead (framebuffer size
was needed only for the scissor's NDC-to-pixel conversion) — removed
them and their one call site in FrameRootComposition.cs. Rewrote
RetailPViewPassExecutor.cs's K4 comment: it no longer asserts a live
scissor mechanism; it states that VulkanGpuPassEncoder's constructor
sets the full-attachment scissor once, at pass begin
(VulkanGpuPassEncoder.cs:87), and nothing narrows it after that. KEPT
per the round-1 verdict: ScissorFallbacks, OutsidePlaneCount,
OutsideViewNdcAabb, HasOutsideView, OutdoorVisible, OutdoorSlot,
ClipViewSlice.NdcAabb (the >8-edge zero-plane slice case the punch fans
still consume) and the VulkanViewportMapping.ScissorToVulkan pass-begin
path (IGpuPassEncoder.SetScissor stays). Also deleted
ClipFrameAssembly.TerrainMode/TerrainScissorNdcAabb and their writers
in ClipFrameAssembler.cs (both Assemble and ReassembleOutsideViewFromWalk);
OutsidePlaneCount's formula reduces to `outsideHasScissorFallback ? 0 :
outsideMaxPlaneCount` without needing the deleted TerrainClipMode
comparison (a scissor-fallback slice can only exist when outdoorVisible
is already true, so the three original cases collapse identically).
Deleted the two "terrain=" / "outMode=" diagnostic reads in
WorldRenderDiagnostics.cs (:345, :409) and the now-vestigial
EmitClipRouteProbe "ubo: n=..." segment that read ClipFrame's deleted
terrain bytes.
DEVIATION from L2's literal text: kept the TerrainClipMode enum type
itself — WorldSceneRenderer.cs's flat-world safety path (explicitly
"out of this chunk's scope" per K4's own round-1 comment) still uses it
for an unrelated "did the flat terrain draw" diagnostic flag, with its
own WorldSceneDiagnosticsController/WorldRenderDiagnostics/test
consumers. Deleting the type would require rewriting files outside
every round's declared file list. Reworded its doc comment to state
this plainly.
L3 (MAJOR, completes K3) — dead TerrainClip UBO deleted from the sky
and terrain shaders. Deleted the block, both gl_ClipDistance loops, and
the gl_PerVertex redeclaration (nothing else needs it) from sky.vert,
terrain_modern.vert, terrain_atmospheric.vert. Recompiled via
tools/compile-shaders.ps1 (glslc via the Vulkan SDK, managed shaderc
fallback also runs) — 24/24 pairs compiled; only sky.vert.spv,
terrain_atmospheric.vert.spv and terrain_modern.vert.spv changed.
Re-pinned VulkanShaderManifestTests.cs's frozen retail-oracle hashes
for sky.vert.spv (7d67a9e3624d198b370d402b5c12e4ce925bf9b8e646ef5123636a86d5985ab5)
and terrain_modern.vert.spv (8a73d89ef0e51e550327b9ff8c24857e309103b1d491030cf0d4d8594b45068c)
with dated comments, matching the existing re-pin convention.
Deleted WorldFrameSectionBinding.BindTerrainClip and
WorldFrameSections.TerrainClip (+ its Reset) with its two callers
(SkyRenderer.Rhi.cs:258, TerrainModernRenderer.Rhi.cs:259). DEVIATION
from L3's literal text: kept the shared Zeroed(...) helper in
WorldPassScope.cs — it is also called by BindSceneLighting and
BindClipRegions, both of which stay; deleting it would have broken
those two live bindings. Deleted IWorldPassSurface.BindTerrainClip
(no-op interface method + RhiWorldPassSurface body) and
WorldScenePassExecutor.cs's two calls to it (:121, :235).
Deleted ClipFrame's _terrainBytes field, TerrainBytes/TerrainBytesForTest
properties, Reset's Array.Clear(_terrainBytes), and the stale
header/K3 comment paragraphs. DEVIATION from L3's literal text: kept
ClipFrame.TerrainUboBytes and ClipFrame.TerrainClipUboBinding —
PortalDepthMaskRenderer.Rhi.cs (:140, :187) is a live production
consumer of both constants for the KEPT exit-seal/punch-fan clip block,
which was never in scope for deletion (portal_depth.vert's own
TerrainClip UBO declaration is the KEEP block, untouched). Reworded
both constants' doc comments (ClipFrame.cs, VulkanPipelineLayouts.cs's
UniformTerrainClip) to say only the portal-depth clip block uses
binding 2 now, and corrected the two GpuBindingModel.cs comments that
explained why terrain-tiling/sky-params bindings are 3/4 by naming "the
terrain clip block". Updated VulkanShaderDescriptorContractTests.cs's
TerrainVertexShaderDeclaresItsClipBlockInTheUniformSet (renamed
TerrainVertexShaderDeclaresOnlySceneLightingInTheUniformSet):
terrain_modern.vert's uniform set is now {SceneLighting} only. Deleted
ClipFrameLayoutTests.NoClip_TerrainBytes_Count0_AllZeros and its K3
comment; corrected the class doc comment and LayoutConstants_MatchShaderStruct's
"terrain UBO"/binding-contract comments to describe the portal-depth
consumer instead.
REQUIRED L3 fact (declared-but-never-bound dynamic uniform binding):
VulkanFrameBindings's constructor seeds EVERY declared uniform binding
(0..UniformBindingCount-1, including binding 2) with the shared dummy
buffer's range before any renderer runs (VulkanFrameBindings.cs:128-130,
`_arena.SeedUniform(binding, dummy.Handle.Handle, dummyUniformRange)`
in a loop over every binding) — "Every binding is always bound, whether
a renderer uses it or not... unused ones point at a shared dummy range"
(VulkanFrameBindings.cs:27-31). So after this round, when the sky or
terrain pipeline draws, set 1's shared descriptor layout still declares
binding 2 (portal_depth.vert's own declaration keeps
IsDeclaredUniformBinding(2) true), and its descriptor still points at a
valid (dummy) range from that seed — vkCmdBindDescriptorSets stays
legal even though neither shader statically uses binding 2 any more.
No SPIR-V-side change was needed to keep this legal.
L4 (MAJOR) — one outside-view slice constructor. Extracted
ClipFrameAssembler.AppendOutsideSlice (frame, ViewPolygon,
outsideSlicesList, ref maxPlaneCount, ref hasScissorFallback, ref
scissorFallbacks) — the exact ClipPlaneSet.From/AppendSlot/
ClipViewSlice-construction body — and call it from both Assemble's
outside_view loop and ReassembleOutsideViewFromWalk's outside_view
loop. Rewrote ClipFrameLayoutTests's K6 pin
(ClipViewSlicePlanes_PunchFanPath_EqualsCpuViewPolygonEdgePlanes_ForASyntheticView):
builds a synthetic WalkPortalView from pixel-space points (the
WalkCopyViewTests pattern) via a trivial IWalkRayCaster, runs
BeginWalkFrame(frame, outdoorRoot: false) then
ReassembleOutsideViewFromWalk(assembly, walkView, 640, 480), and reads
assembly.OutsideViewSlices[0].Planes — the exact production pair
RetailPViewPassExecutor.cs's BeginWalkFrame call and
RetailPViewRenderer.cs's ReassembleOutsideViewFromWalk call make.
Mutation (perturbed planes[0].W inside the shared helper, right after
`planes = cps.PlaneArray;`):
edge 0 midpoint (-0.3,0.10000001) should lie ~on its OWN GPU plane; the closest plane was only 0.5251073 away
The pin's own source contains no `Assemble(` call — grep-checked by
extracting the method body and searching it for the literal text; no
match.
L5 — shader comments corrected. sky.vert/terrain_modern.vert/
terrain_atmospheric.vert now state retail draws the sky once
(LScape::draw @0x00506330 -> GameSky::Draw(sky,0) @0x0050633c, before
draw_check_blocks) and the landscape is view-culled per cell, never
GPU-clipped (RenderDeviceD3D::DrawBlock @0x005a17c0). The "Phase W
Stage 4"/"U.3 default" clip narratives are deleted along with the
blocks they described.
L6 — grep sweep. `grep -rn "<token>" src tests docs/architecture`
returns nothing for SetTerrainClip, BeginDoorwayScissor,
BindTerrainClip, TerrainBytes, "active scissor", and NdcScissorRect —
confirmed after this commit (docs/plans and docs/research keep the
historical record, untouched). TerrainClipMode is the one deliberate
exception (see the L2 deviation note above); every one of its 9
remaining hits is a live, non-stale reference (the enum declaration,
WorldSceneRenderer's flat-path local, or their diagnostic/test
plumbing), not a stale mention of a deleted mechanism.
L7 — Issue130DoorwayStripTests. Deleted AnySliceAdmitsScissor,
worstScissorGapPx and its PIN 1 assertion, the header's scissor
sentences, and the scissorGap half of MeasureTopEdgeGap (dropped the
fbW parameter it alone needed). Rewrote the header: the scissor
mechanism is retired (the sky and landscape draw unclipped; aperture
exactness comes from the depth clear, the exit seals and the interior
repaint). The remaining plane-gap half (the canary PIN,
`worstPlaneGapPx <= 1.2f`) still pins something production reads:
AnySliceAdmitsPlanes walks slice.Planes from the SAME ProjectToClip ->
ClipToRegion -> ClipPlaneSet.From pipeline
RetailPViewPassExecutor.DrawWalkPunchFan reads through
clipAssembly.OutsideViewSlices[activeViewIndex].Planes — so the test
was kept, not deleted.
L8 — DrawWalkSky loop-shape pin. Added
DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch (same
backward-branch-span shape as K1's DrawWeatherOnce pin), plus a note in
both pins that CompiledCallGraph.ReadBranches does not decode a
compiled switch jump table, but no C# loop construct compiles to one.
Mutation (wrapped the RenderSky call in `for (int i = 0; i < 2; i++)`):
Assert.DoesNotContain() Failure: Filter matched in collection
Collection: [..., CompiledBranch { Offset = 20, OpCode = brtrue.s, TargetOffset = 25 }, CompiledBranch { Offset = 23, OpCode = br.s, TargetOffset = 66 }, CompiledBranch { Offset = 72, OpCode = blt.s, TargetOffset = 13 }, CompiledBranch { Offset = 80, OpCode = brfalse.s, TargetOffset = 116 }, CompiledBranch { Offset = 88, OpCode = brfalse.s, TargetOffset = 116 }]
(the Offset=72 -> TargetOffset=13 entry is the injected loop's backward
branch spanning the call).
L9 — this message.
Gates: dotnet build 0 warnings / 0 errors. Hermetic filter
(Lane!=InstalledDat&...&Status!=KnownFailure): 6827 passed, 0 failed.
InstalledDat lane against a real DAT directory: 244 passed, 4 failed —
exactly the four known failures (LayoutImporterMediaBearingChildSweepTests
+ LayoutImporterInvisibleSweepTests, both #383; TowerAscentReplayTests,
KnownFailure; WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff,
#458 KnownFailure). No register row added (every deletion here removes
an acdream-only rule; the KEEP items already had their equivalence
pins from round 1). No new flag/probe.
OWED: the lead's ACDREAM_DEVTOOLS=1 four-pose visual self-gate against
the running client is not run by this agent — CLAUDE.md and this
task's own instructions forbid launching the graphical client from
here; that visual/validation-layer pass remains the user's to run
before this round is accepted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
576 lines
23 KiB
C#
576 lines
23 KiB
C#
// ClipFrameAssembler.cs
|
||
//
|
||
// Retail view assembly policy. The production frame walk supplies its
|
||
// outside_view directly; the legacy PortalVisibilityBuilder overload remains
|
||
// only for isolated research/replay tests. Each visible polygon is packed as
|
||
// an individual GPU clip slot.
|
||
//
|
||
// outside_view landscape slices
|
||
// reverse cell_draw_list exit masks
|
||
// reverse cell_draw_list EnvCell shells
|
||
// reverse cell_draw_list object lists
|
||
//
|
||
// Slot 0 is always no-clip. A slice whose polygon cannot be represented by the
|
||
// <=8 plane budget uses slot 0 and its NDC AABB; the renderer uses scissor for
|
||
// passes that need that fallback. Empty regions are omitted entirely.
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.App.Rendering;
|
||
|
||
/// <summary>
|
||
/// S3 chunk 4 fix round 2 (L2): retired from the walk's own clip assembly —
|
||
/// <see cref="ClipFrameAssembly"/> no longer tracks a terrain clip mode at
|
||
/// all, because the walk draws the sky, terrain and weather unclipped, one
|
||
/// call each, matching retail (<c>LScape::draw</c> never installs a view
|
||
/// before any of the three). The type stays only because
|
||
/// <see cref="WorldSceneRenderer"/>'s separate flat-world safety path still
|
||
/// uses it for its own unrelated "did the flat terrain draw this frame"
|
||
/// diagnostic flag (<c>Planes</c> = drew, <c>Skip</c> = the PView walk ran
|
||
/// instead; that path never produces <c>Scissor</c>).
|
||
/// </summary>
|
||
public enum TerrainClipMode
|
||
{
|
||
/// <summary>All outside_view slices have convex plane clips.</summary>
|
||
Planes,
|
||
|
||
/// <summary>At least one outside_view slice requires scissor fallback.</summary>
|
||
Scissor,
|
||
|
||
/// <summary>No outside_view slice is visible; skip landscape indoors.</summary>
|
||
Skip,
|
||
}
|
||
|
||
/// <summary>
|
||
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained
|
||
/// for passes that cannot write gl_ClipDistance and must use scissor.
|
||
/// </summary>
|
||
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
|
||
|
||
/// <summary>
|
||
/// Populated clip buffers plus routing data consumed by the render walk.
|
||
/// </summary>
|
||
public sealed class ClipFrameAssembly
|
||
{
|
||
public ClipFrame Frame { get; private set; } = null!;
|
||
|
||
/// <summary>First drawable slice slot per visible cell. Compatibility map
|
||
/// for renderer APIs that can accept only one slot at a time.</summary>
|
||
public Dictionary<uint, int> CellIdToSlot { get; } = new();
|
||
|
||
/// <summary>Slot-only cell slices, retained for older renderer APIs.</summary>
|
||
public Dictionary<uint, int[]> CellIdToViewSlots { get; } = new();
|
||
|
||
/// <summary>Full retail portal_view slices per visible cell.</summary>
|
||
public Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; } = new();
|
||
|
||
/// <summary>Full retail outside_view slices.</summary>
|
||
public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty<ClipViewSlice>();
|
||
|
||
public int OutdoorSlot { get; internal set; }
|
||
public bool OutdoorVisible { get; internal set; }
|
||
public bool HasOutsideView { get; internal set; }
|
||
public Vector4 OutsideViewNdcAabb { get; internal set; }
|
||
|
||
// Probe data.
|
||
public int OutsidePlaneCount { get; internal set; }
|
||
public Dictionary<uint, int> PerCellPlaneCounts { get; } = new();
|
||
public int ScissorFallbacks { get; internal set; }
|
||
|
||
// The assembly is frame-scoped. An owner that passes it back to Assemble may reuse the
|
||
// dictionaries, per-cell arrays, and construction list after the prior frame is consumed.
|
||
// Exact-length pools keep the public array API honest: Length is always the live slice count.
|
||
private readonly Dictionary<int, Stack<ClipViewSlice[]>> _sliceArraysByLength = new();
|
||
private readonly Dictionary<int, Stack<int[]>> _slotArraysByLength = new();
|
||
internal List<ClipViewSlice> SliceScratch { get; } = new();
|
||
internal int SliceArrayAllocationCount { get; private set; }
|
||
internal int SlotArrayAllocationCount { get; private set; }
|
||
internal const int MaxRetainedSliceItems = 4096;
|
||
internal const int MaxRetainedSlotItems = 8192;
|
||
internal const int MaxRetainedArraysPerPool = 128;
|
||
internal int RetainedSliceItems { get; private set; }
|
||
internal int RetainedSlotItems { get; private set; }
|
||
internal int RetainedSliceArrays { get; private set; }
|
||
internal int RetainedSlotArrays { get; private set; }
|
||
|
||
internal void Reset(ClipFrame frame)
|
||
{
|
||
Frame = frame;
|
||
foreach (ClipViewSlice[] slices in CellIdToViewSlices.Values)
|
||
ReturnSlices(slices);
|
||
foreach (int[] slots in CellIdToViewSlots.Values)
|
||
ReturnSlots(slots);
|
||
if (OutsideViewSlices.Length != 0)
|
||
ReturnSlices(OutsideViewSlices);
|
||
|
||
CellIdToSlot.Clear();
|
||
CellIdToViewSlots.Clear();
|
||
CellIdToViewSlices.Clear();
|
||
PerCellPlaneCounts.Clear();
|
||
OutsideViewSlices = System.Array.Empty<ClipViewSlice>();
|
||
SliceScratch.Clear();
|
||
}
|
||
|
||
internal ClipViewSlice[] CopySlices(List<ClipViewSlice> source)
|
||
{
|
||
if (source.Count == 0)
|
||
return System.Array.Empty<ClipViewSlice>();
|
||
ClipViewSlice[] result = RentSlices(source.Count);
|
||
source.CopyTo(result, 0);
|
||
return result;
|
||
}
|
||
|
||
internal int[] CopySlots(ClipViewSlice[] slices)
|
||
{
|
||
if (slices.Length == 0)
|
||
return System.Array.Empty<int>();
|
||
int[] result = RentSlots(slices.Length);
|
||
for (int i = 0; i < slices.Length; i++)
|
||
result[i] = slices[i].Slot;
|
||
return result;
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (_sliceArraysByLength.TryGetValue(length, out Stack<ClipViewSlice[]>? pool)
|
||
&& pool.Count != 0)
|
||
{
|
||
ClipViewSlice[] result = pool.Pop();
|
||
RetainedSliceItems -= result.Length;
|
||
RetainedSliceArrays--;
|
||
if (pool.Count == 0)
|
||
_sliceArraysByLength.Remove(length);
|
||
return result;
|
||
}
|
||
SliceArrayAllocationCount++;
|
||
return new ClipViewSlice[length];
|
||
}
|
||
|
||
private int[] RentSlots(int length)
|
||
{
|
||
if (_slotArraysByLength.TryGetValue(length, out Stack<int[]>? pool)
|
||
&& pool.Count != 0)
|
||
{
|
||
int[] result = pool.Pop();
|
||
RetainedSlotItems -= result.Length;
|
||
RetainedSlotArrays--;
|
||
if (pool.Count == 0)
|
||
_slotArraysByLength.Remove(length);
|
||
return result;
|
||
}
|
||
SlotArrayAllocationCount++;
|
||
return new int[length];
|
||
}
|
||
|
||
private void ReturnSlices(ClipViewSlice[] array)
|
||
{
|
||
// ClipViewSlice contains a Vector4[] reference. Clear before either retaining or dropping
|
||
// the array so a bounded cache cannot accidentally extend plane-payload lifetimes.
|
||
System.Array.Clear(array);
|
||
if (array.Length > MaxRetainedSliceItems)
|
||
return;
|
||
while (RetainedSliceItems + array.Length > MaxRetainedSliceItems
|
||
|| RetainedSliceArrays >= MaxRetainedArraysPerPool)
|
||
{
|
||
if (!EvictOneSliceArray())
|
||
break;
|
||
}
|
||
if (!_sliceArraysByLength.TryGetValue(array.Length, out Stack<ClipViewSlice[]>? pool))
|
||
{
|
||
pool = new Stack<ClipViewSlice[]>();
|
||
_sliceArraysByLength.Add(array.Length, pool);
|
||
}
|
||
pool.Push(array);
|
||
RetainedSliceItems += array.Length;
|
||
RetainedSliceArrays++;
|
||
}
|
||
|
||
private void ReturnSlots(int[] array)
|
||
{
|
||
if (array.Length > MaxRetainedSlotItems)
|
||
return;
|
||
while (RetainedSlotItems + array.Length > MaxRetainedSlotItems
|
||
|| RetainedSlotArrays >= MaxRetainedArraysPerPool)
|
||
{
|
||
if (!EvictOneSlotArray())
|
||
break;
|
||
}
|
||
if (!_slotArraysByLength.TryGetValue(array.Length, out Stack<int[]>? pool))
|
||
{
|
||
pool = new Stack<int[]>();
|
||
_slotArraysByLength.Add(array.Length, pool);
|
||
}
|
||
pool.Push(array);
|
||
RetainedSlotItems += array.Length;
|
||
RetainedSlotArrays++;
|
||
}
|
||
|
||
private bool EvictOneSliceArray()
|
||
{
|
||
int selectedLength = -1;
|
||
foreach ((int length, Stack<ClipViewSlice[]> pool) in _sliceArraysByLength)
|
||
{
|
||
if (pool.Count != 0 && length > selectedLength)
|
||
selectedLength = length;
|
||
}
|
||
if (selectedLength < 0)
|
||
return false;
|
||
Stack<ClipViewSlice[]> selected = _sliceArraysByLength[selectedLength];
|
||
ClipViewSlice[] evicted = selected.Pop();
|
||
RetainedSliceItems -= evicted.Length;
|
||
RetainedSliceArrays--;
|
||
if (selected.Count == 0)
|
||
_sliceArraysByLength.Remove(selectedLength);
|
||
return true;
|
||
}
|
||
|
||
private bool EvictOneSlotArray()
|
||
{
|
||
int selectedLength = -1;
|
||
foreach ((int length, Stack<int[]> pool) in _slotArraysByLength)
|
||
{
|
||
if (pool.Count != 0 && length > selectedLength)
|
||
selectedLength = length;
|
||
}
|
||
if (selectedLength < 0)
|
||
return false;
|
||
Stack<int[]> selected = _slotArraysByLength[selectedLength];
|
||
int[] evicted = selected.Pop();
|
||
RetainedSlotItems -= evicted.Length;
|
||
RetainedSlotArrays--;
|
||
if (selected.Count == 0)
|
||
_slotArraysByLength.Remove(selectedLength);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public static class ClipFrameAssembler
|
||
{
|
||
/// <summary>
|
||
/// Starts one production walk assembly without constructing a parallel
|
||
/// PortalVisibilityFrame. Outdoor roots begin with retail's full-screen
|
||
/// default view; interior roots begin empty and are populated from
|
||
/// <see cref="Walk.RetailFrameWalk.InteriorOutsideView"/> after Collect.
|
||
/// </summary>
|
||
public static ClipFrameAssembly BeginWalkFrame(
|
||
ClipFrame frame,
|
||
bool outdoorRoot,
|
||
ClipFrameAssembly? reuseAssembly = null)
|
||
{
|
||
System.ArgumentNullException.ThrowIfNull(frame);
|
||
frame.Reset();
|
||
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
|
||
assembly.Reset(frame);
|
||
|
||
if (outdoorRoot)
|
||
{
|
||
List<ClipViewSlice> slices = assembly.SliceScratch;
|
||
slices.Clear();
|
||
var fullScreen = new Vector4(-1f, -1f, 1f, 1f);
|
||
slices.Add(new ClipViewSlice(0, fullScreen, System.Array.Empty<Vector4>()));
|
||
assembly.SetOutsideViewSlices(assembly.CopySlices(slices));
|
||
assembly.OutdoorSlot = 0;
|
||
assembly.OutdoorVisible = true;
|
||
assembly.HasOutsideView = true;
|
||
assembly.OutsideViewNdcAabb = fullScreen;
|
||
assembly.OutsidePlaneCount = 0;
|
||
assembly.ScissorFallbacks = 1;
|
||
}
|
||
else
|
||
{
|
||
assembly.OutdoorSlot = 0;
|
||
assembly.OutdoorVisible = false;
|
||
assembly.HasOutsideView = false;
|
||
assembly.OutsideViewNdcAabb = Vector4.Zero;
|
||
assembly.OutsidePlaneCount = 0;
|
||
assembly.ScissorFallbacks = 0;
|
||
}
|
||
|
||
return assembly;
|
||
}
|
||
|
||
public static ClipFrameAssembly Assemble(
|
||
ClipFrame frame,
|
||
PortalVisibilityFrame pvFrame,
|
||
ClipFrameAssembly? reuseAssembly = null)
|
||
{
|
||
System.ArgumentNullException.ThrowIfNull(frame);
|
||
System.ArgumentNullException.ThrowIfNull(pvFrame);
|
||
|
||
frame.Reset();
|
||
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
|
||
assembly.Reset(frame);
|
||
|
||
Dictionary<uint, int> cellIdToSlot = assembly.CellIdToSlot;
|
||
Dictionary<uint, int[]> cellIdToViewSlots = assembly.CellIdToViewSlots;
|
||
Dictionary<uint, ClipViewSlice[]> cellIdToViewSlices = assembly.CellIdToViewSlices;
|
||
Dictionary<uint, int> perCellPlaneCounts = assembly.PerCellPlaneCounts;
|
||
int scissorFallbacks = 0;
|
||
|
||
foreach (uint cellId in pvFrame.OrderedVisibleCells)
|
||
{
|
||
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
|
||
continue;
|
||
|
||
List<ClipViewSlice> slices = assembly.SliceScratch;
|
||
slices.Clear();
|
||
int maxPlaneCount = 0;
|
||
|
||
foreach (var poly in view.Polygons)
|
||
{
|
||
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 > maxPlaneCount)
|
||
maxPlaneCount = cps.Count;
|
||
}
|
||
else
|
||
{
|
||
planes = System.Array.Empty<Vector4>();
|
||
slot = 0;
|
||
scissorFallbacks++;
|
||
}
|
||
|
||
slices.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
|
||
}
|
||
|
||
if (slices.Count == 0)
|
||
continue;
|
||
|
||
ClipViewSlice[] sliceArray = assembly.CopySlices(slices);
|
||
cellIdToViewSlices[cellId] = sliceArray;
|
||
cellIdToViewSlots[cellId] = assembly.CopySlots(sliceArray);
|
||
cellIdToSlot[cellId] = sliceArray[0].Slot;
|
||
perCellPlaneCounts[cellId] = maxPlaneCount;
|
||
}
|
||
|
||
List<ClipViewSlice> outsideSlicesList = assembly.SliceScratch;
|
||
outsideSlicesList.Clear();
|
||
int outsideMaxPlaneCount = 0;
|
||
bool outsideHasScissorFallback = false;
|
||
|
||
foreach (var poly in pvFrame.OutsideView.Polygons)
|
||
{
|
||
AppendOutsideSlice(
|
||
frame,
|
||
poly,
|
||
outsideSlicesList,
|
||
ref outsideMaxPlaneCount,
|
||
ref outsideHasScissorFallback,
|
||
ref scissorFallbacks);
|
||
}
|
||
|
||
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
|
||
bool outdoorVisible = outsideViewSlices.Length > 0;
|
||
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
|
||
|
||
Vector4 outsideViewNdcAabb = outdoorVisible
|
||
? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY,
|
||
pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY)
|
||
: Vector4.Zero;
|
||
|
||
assembly.SetOutsideViewSlices(outsideViewSlices);
|
||
assembly.OutdoorSlot = outdoorSlot;
|
||
assembly.OutdoorVisible = outdoorVisible;
|
||
assembly.HasOutsideView = outdoorVisible;
|
||
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
|
||
// S3 chunk 4 fix round 2 (L2): equivalent to the deleted
|
||
// terrainMode == TerrainClipMode.Planes gate without needing
|
||
// TerrainClipMode at all — a scissor-fallback slice can only exist
|
||
// when outdoorVisible is already true (both branches above append a
|
||
// slice), so this reduces to the same three cases (no slices -> 0;
|
||
// any scissor fallback -> 0; all-planes -> outsideMaxPlaneCount).
|
||
assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
|
||
assembly.ScissorFallbacks = scissorFallbacks;
|
||
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)(w−y_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;
|
||
}
|
||
|
||
AppendOutsideSlice(
|
||
frame,
|
||
poly,
|
||
outsideSlicesList,
|
||
ref outsideMaxPlaneCount,
|
||
ref outsideHasScissorFallback,
|
||
ref scissorFallbacks);
|
||
}
|
||
|
||
ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList);
|
||
bool outdoorVisible = outsideViewSlices.Length > 0;
|
||
int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0;
|
||
|
||
Vector4 outsideViewNdcAabb = outdoorVisible
|
||
? new Vector4(unionMinX, unionMinY, unionMaxX, unionMaxY)
|
||
: Vector4.Zero;
|
||
|
||
assembly.SetOutsideViewSlices(outsideViewSlices);
|
||
assembly.OutdoorSlot = outdoorSlot;
|
||
assembly.OutdoorVisible = outdoorVisible;
|
||
assembly.HasOutsideView = outdoorVisible;
|
||
assembly.OutsideViewNdcAabb = outsideViewNdcAabb;
|
||
// S3 chunk 4 fix round 2 (L2): see Assemble's matching comment — the
|
||
// same reduction applies here.
|
||
assembly.OutsidePlaneCount = outsideHasScissorFallback ? 0 : outsideMaxPlaneCount;
|
||
assembly.ScissorFallbacks = scissorFallbacks;
|
||
}
|
||
|
||
/// <summary>
|
||
/// S3 chunk 4 fix round 2 (L4): the ONE place a single outside_view
|
||
/// <see cref="ViewPolygon"/> becomes an appended clip slot plus its
|
||
/// <see cref="ClipViewSlice"/> — both <see cref="Assemble"/>'s
|
||
/// outside_view loop and <see cref="ReassembleOutsideViewFromWalk"/>'s
|
||
/// outside_view loop call this instead of each carrying its own copy of
|
||
/// the plane/scissor-fallback bookkeeping (a prior round's three-lens
|
||
/// review found the duplication let a CPU/GPU equivalence pin exercise
|
||
/// one copy while production ran the other — see <c>ClipFrameLayoutTests</c>'
|
||
/// punch-fan pin). A polygon entirely outside every plane
|
||
/// (<see cref="ClipPlaneSet.IsNothingVisible"/>) appends nothing and
|
||
/// returns false; the caller's own loop simply moves to the next
|
||
/// polygon either way, so the return value only matters to a caller that
|
||
/// needs to know.
|
||
/// </summary>
|
||
private static bool AppendOutsideSlice(
|
||
ClipFrame frame,
|
||
in ViewPolygon poly,
|
||
List<ClipViewSlice> outsideSlicesList,
|
||
ref int maxPlaneCount,
|
||
ref bool hasScissorFallback,
|
||
ref int scissorFallbacks)
|
||
{
|
||
var cps = ClipPlaneSet.From(poly);
|
||
if (cps.IsNothingVisible)
|
||
return false;
|
||
|
||
int slot;
|
||
Vector4[] planes;
|
||
if (cps.Count > 0)
|
||
{
|
||
planes = cps.PlaneArray;
|
||
slot = frame.AppendSlot(planes);
|
||
if (cps.Count > maxPlaneCount)
|
||
maxPlaneCount = cps.Count;
|
||
}
|
||
else
|
||
{
|
||
planes = System.Array.Empty<Vector4>();
|
||
slot = 0;
|
||
hasScissorFallback = true;
|
||
scissorFallbacks++;
|
||
}
|
||
|
||
outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes));
|
||
return true;
|
||
}
|
||
|
||
private static Vector4 AabbOf(ViewPolygon poly) =>
|
||
new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY);
|
||
|
||
}
|