feat(render) Campaign FW3.2b-2: THE STATIC CUTOVER - the walk drives production statics
The retail frame walk now drives every production static draw. In RetailPViewRenderer.DrawInside, when the concrete executor + the packed product + the FW3.1 walk registries are all wired (all production compositions - anything less throws): - A pre-walk events-only collection pass (the shadow sink generalized to WalkVisitedSetCollector) gathers the frame's visited cells, buildings, and landscape-cell turns; the visited cells union into prepareCells so EnvCellRenderer prepares every shell the driver draws. - DrawWalkDrivenStatics runs the WalkFrameDriver over the production world data (WalkProductionWorldData over RenderSceneQuery + the building registry): sky, terrain slices, outdoor statics at their landscape-cell turns, buildings (alpha barrier -> punch/look-in passes -> shell) in retail order, interior clear+seals as leaf closures (the old tail block's drain reasoning moves with them), flood cells shell-then-contents. Landscape/cell-stage particle owners re-source from the walk's visited sets - retail gates particles per cell turn (ShouldDrawParticles @0x0050FE60), which this is; the old sphere filter was the approximation. - DrawLandscapeDynamicsPhase + DrawBuildingLookInDynamics carry the dynamics-only remainder (LookInObject now dynamic-classified, late outside-dynamics + weather, particle unions); DrawDynamicsLast and the outdoor flush are unchanged. - The product builder stops emitting LandscapeOutdoorStatic / LandscapeBuildingShell / CellStatic (methods deleted, dead index tracking removed); LookInObject loads cells with includeStatics: false. The old static path survives ONLY behind !walkActive for the standalone/diagnostic executor-fake path that keeps 15 retail-ordering regression tests exercising the barrier/punch/seal machinery; no production composition can reach it. Its deletion is FW4 scope (the plan's "deleting the patch apparatus") - recorded in the plan. Transitional risks recorded in code/report: the two-pass walk cost (FW3.4 measures), the interior slice-count reconciliation between the old clip assembly and the walk's own exit-view survival, and the outdoor merged-flood punch coverage now riding the walk's own building-BSP punches (retail-faithful per FW1; the owner visual gate verifies). Suites (lead-verified): full Release build 0 warnings; hermetic 6,750/0 (baseline minus the three deleted route tests); Walk lane 201/1; InstalledDat Walk conformance 40/1 untouched. The two IL-branch tests the implementation round reported failing pass in every lead run - the recurring parallel-load flake pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
878533597d
commit
4918677b45
8 changed files with 919 additions and 612 deletions
|
|
@ -343,6 +343,63 @@ deeper commitment.
|
|||
landscape turn; the `_drawCullModes`/alpha-scope sequencing constraint
|
||||
resolved (own cull scratch or strict sequencing). Route-keyed particle
|
||||
owners re-sourced from the walk.
|
||||
- **FW3.2b-2 — the production rooting (DESIGN, 2026-08-30; execute
|
||||
INLINE, not via subagent — the integration-slice rule).** The
|
||||
dual-compute split for this stage: the OLD visibility
|
||||
(`PortalVisibilityBuilder` + look-in frames + clip assembly +
|
||||
viewcone) KEEPS RUNNING to feed everything dynamic; the WALK draws
|
||||
everything static. No old static DRAWS remain (no double draw):
|
||||
1. `RetailPViewRenderer.DrawInside` keeps steps 1–8 and 10–11
|
||||
(visibility, merge/look-in seeding, clip frame, prepare — with
|
||||
`prepareCells` UNIONED with the walk's flood/look-in cells —
|
||||
viewcone, route build) but `RenderScenePViewFrameProduct` STOPS
|
||||
emitting `LandscapeOutdoorStatic`, `LandscapeBuildingShell`, and
|
||||
`CellStatic` ranges. Per the plan's route split, LOOK-IN OBJECTS
|
||||
(`LookInObject`) are dynamic-side and STAY PACKED this stage —
|
||||
the old look-in machinery keeps drawing look-in cell content and
|
||||
its punches; the WALK's building turns therefore draw shell
|
||||
content + punches for NON-look-in... NO — resolve the overlap the
|
||||
other way: the walk's building turns run their punch+look-in
|
||||
machinery ONLY for the walk's own conformance events; production
|
||||
look-in DRAWS (punch fans + content) stay entirely on the old
|
||||
path this stage, and the walk's `BuildingShell` stage content +
|
||||
`OnPunchGeometry`/look-in DC draws are SUPPRESSED in the driver's
|
||||
production wiring for look-in-owned buildings… REJECTED — that is
|
||||
a dual path. FINAL: the walk owns ALL of it — building shells,
|
||||
punches, look-in STATIC content (stage LookInStatic) — and the
|
||||
`LookInObject` route survives ONLY for the DYNAMICS inside
|
||||
look-in cells (`BuildLookInRoutes` filtered to dynamic
|
||||
classifications). The old `DrawBuildingLookIns` static half and
|
||||
`DrawLookInPortalPunch` retire with the static routes.
|
||||
2. `WalkFrameDriver`'s leaf renderer maps to:
|
||||
sky → `RetailPViewPassExecutor` sky call; terrain slice →
|
||||
`_terrain.Draw(camera, frustum, clipPlanes: slice.Planes,
|
||||
ndcClipAabb: slice.NdcAabb)`; cell shell →
|
||||
`EnvCellRenderer.Render(pass, singleCellSet)` (+ transparent
|
||||
ordered per cell); punch fan → `PortalDepthMaskRenderer.
|
||||
DrawDepthFan(worldVerts, viewProjection, slicePlanes,
|
||||
forceFarZ: true)`; `ClearInteriorDepth` →
|
||||
`IWorldPassScope.ClearInteriorDepth` (keep production's
|
||||
slices>0 gate); `DrawExitSeals` → the existing seal-fan loop
|
||||
(`DrawPortalDepthWrite` seal flavor over the root flood's exit
|
||||
portals); alpha barrier → `RetailAlphaQueue.FlushFartherThan`.
|
||||
3. Static records: cell statics via `RenderSceneQuery.
|
||||
CopyCellStaticsTo(cellId)`; outdoor statics via a per-landscape-
|
||||
cell index built once per frame from
|
||||
`CopyIndexTo(OutdoorStatic)`; building shells via the
|
||||
`IsBuildingShell` entities keyed by anchor cell.
|
||||
4. Consumers re-pointed: particle owners
|
||||
(`RenderFrameRouteOwnerSelector.Replace(LandscapeOutdoorStatic…)`
|
||||
sites) re-source from the walk's populated cells; diagnostics
|
||||
source counts tolerate the route removal; `EmitRenderSignature`'s
|
||||
terrain/scenery counts re-source or annotate.
|
||||
5. Sky/weather: sky moves into the walk's LS turn (once); weather
|
||||
stays in the old late-dynamics slice loop this stage (it is
|
||||
dynamic-adjacent and the plan's FW4 moves it fully).
|
||||
6. Gates before the owner sees a build: hermetic + InstalledDat
|
||||
suites; the connected lifecycle route; the offline pixel gate
|
||||
(outdoor statics); dense-Arwic pre/post pair (physical display).
|
||||
|
||||
- **FW3.3 — ShellDrawLiftZ retirement.** THREE sites (the register row
|
||||
AP-32 lists only two — `LandblockBuildFactory.cs` bakes the lift into
|
||||
the drawn cell transform and is unlisted): delete all three, flip the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Sky;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
|
|
@ -181,6 +182,25 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
_particleClassifications.BeginFrame();
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the shared dispatcher, for
|
||||
/// <see cref="RetailPViewRenderer"/>'s <c>WalkFrameDriver</c>
|
||||
/// construction — the driver's ctor takes a <see cref="WbDrawDispatcher"/>
|
||||
/// directly (it calls <c>SubmitOrderedStream</c> itself; see that
|
||||
/// class's own doc comment).</summary>
|
||||
internal WbDrawDispatcher Dispatcher => _entities;
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: forwards to
|
||||
/// <see cref="WbDrawDispatcher.RequireWalkSubmission"/> — the frame/
|
||||
/// encoder pair the walk driver submits its stream flushes into.</summary>
|
||||
internal (IGpuFrame Frame, IGpuPassEncoder Encoder) RequireWalkSubmission() =>
|
||||
_entities.RequireWalkSubmission();
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: forwards to
|
||||
/// <see cref="WbDrawDispatcher.WalkAttachmentExtent"/> — the real
|
||||
/// viewport size for <c>WalkProductionFrameContext</c>.</summary>
|
||||
internal (int Width, int Height)? WalkAttachmentExtent =>
|
||||
_entities.WalkAttachmentExtent;
|
||||
|
||||
public void BeginEntityFrame(in RenderFrameView view) =>
|
||||
_entities.BeginPackedProductionFrame(in view);
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,12 @@ public sealed class RetailPViewRenderer
|
|||
private readonly Walk.WalkLandscapeAssembler? _walkLandscape;
|
||||
private readonly CellVisibility? _walkCellRegistry;
|
||||
|
||||
// Campaign FW3.2b-2: the production IWalkFrameWorldData over the retained
|
||||
// scene — owned here (not just inside the driver) because DrawInside also
|
||||
// reads it directly to re-source particle owners for the routes the walk
|
||||
// now draws (plan §FW3 item 4). Non-null exactly when _walkBuildings is.
|
||||
private readonly Walk.WalkProductionWorldData? _walkWorldData;
|
||||
|
||||
internal RetailPViewRenderer(
|
||||
InteriorEntityPartition.IObserver? partitionObserver,
|
||||
RenderScenePViewFrameProductController? sceneFrameProduct = null,
|
||||
|
|
@ -111,6 +117,9 @@ public sealed class RetailPViewRenderer
|
|||
_walkBuildings = walkBuildings;
|
||||
_walkLandscape = walkLandscape;
|
||||
_walkCellRegistry = walkCellRegistry;
|
||||
_walkWorldData = walkBuildings is not null
|
||||
? new Walk.WalkProductionWorldData(walkBuildings)
|
||||
: null;
|
||||
_partitionObserver = partitionObserver;
|
||||
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
|
||||
_sceneFrameProduct = sceneFrameProduct;
|
||||
|
|
@ -187,6 +196,90 @@ public sealed class RetailPViewRenderer
|
|||
var drawableCells = _drawableCellsScratch;
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Campaign FW3.2b-2: the production rooting. A concrete executor is
|
||||
// required — the walk submits through WbDrawDispatcher.SubmitOrderedStream
|
||||
// and needs a real GPU frame/encoder (RequireWalkSubmission), which no
|
||||
// test IRetailPViewPassExecutor fake can supply. Whenever a concrete
|
||||
// executor IS present, the packed entity-route product must ALSO be
|
||||
// wired with all three walk registries — a scene product without the
|
||||
// walk data (or vice versa) is a production miswiring, not a
|
||||
// legacy/diagnostic shape, so it fails loud rather than silently
|
||||
// falling back to the retired static routes (plan §FW3 item 6).
|
||||
RetailPViewPassExecutor? walkExecutor = passes as RetailPViewPassExecutor;
|
||||
bool walkRegistriesReady =
|
||||
_walkBuildings is not null
|
||||
&& _walkLandscape is not null
|
||||
&& _walkCellRegistry is not null
|
||||
&& _walkWorldData is not null;
|
||||
if (walkExecutor is not null
|
||||
&& _sceneFrameProduct is not null
|
||||
&& !walkRegistriesReady)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"RetailPViewRenderer has a concrete pass executor and a "
|
||||
+ "RenderScenePViewFrameProductController but the walk registries "
|
||||
+ "(WalkBuildingRegistry/WalkLandscapeAssembler/CellVisibility) are "
|
||||
+ "not all wired — the Campaign FW3.2b-2 static cutover requires "
|
||||
+ "every piece together; see FrameRootComposition's "
|
||||
+ "RetailPViewRenderer construction.");
|
||||
}
|
||||
bool walkActive =
|
||||
walkExecutor is not null
|
||||
&& _sceneFrameProduct is not null
|
||||
&& walkRegistriesReady;
|
||||
|
||||
// Campaign FW3.2b-2 pre-walk collection pass: run the production walk
|
||||
// EVENTS-ONLY (no leaf draws) to learn the flood cell set it will draw
|
||||
// this frame, BEFORE prepareCells is finalized below — so EnvCellRenderer
|
||||
// prepares batches for every shell the driver will draw later in this
|
||||
// same DrawInside call. The same WalkProductionFrameContext + camera
|
||||
// cell resolved here are reused for the real driven run further down
|
||||
// (plan §FW3 "FW3.2b-2 — the production rooting", item 1).
|
||||
Walk.WalkProductionFrameContext? walkContext = null;
|
||||
Walk.WalkLandscape? walkLandscape = null;
|
||||
Walk.WalkCell? walkCameraCell = null;
|
||||
if (walkActive)
|
||||
{
|
||||
Matrix4x4 view = ctx.CameraView;
|
||||
var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33));
|
||||
(int Width, int Height)? attachment = walkExecutor!.WalkAttachmentExtent;
|
||||
// Fallback matches the FW3.2b-2 shadow probe's own comment: every
|
||||
// screen projection shares the same constants, so this is only
|
||||
// reached before the world pass has published its scope.
|
||||
float viewportWidth = attachment?.Width ?? 1024f;
|
||||
float viewportHeight = attachment?.Height ?? 720f;
|
||||
walkContext = new Walk.WalkProductionFrameContext(
|
||||
_walkCellRegistry!,
|
||||
_walkBuildings!,
|
||||
ctx.ViewerEyePos,
|
||||
forward,
|
||||
ctx.ViewProjection,
|
||||
viewportWidth,
|
||||
viewportHeight);
|
||||
_walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos);
|
||||
walkLandscape = _walkLandscape.Landscape;
|
||||
|
||||
if ((ctx.ViewerCellId & 0xFFFFu) >= 0x100)
|
||||
{
|
||||
walkCameraCell = _walkCellRegistry!.TryGetCell(ctx.ViewerCellId, out LoadedCell? loaded)
|
||||
? loaded?.Walk
|
||||
: null;
|
||||
if (walkCameraCell is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"walk root=0x{ctx.ViewerCellId:X8}: the interior camera cell has "
|
||||
+ "no committed walk data — the Campaign FW3.2b-2 static cutover "
|
||||
+ "requires the walk registry to already hold the viewer's own cell "
|
||||
+ "(fail-loud rule; a silently skipped root would leave the frame "
|
||||
+ "with no static draws at all).");
|
||||
}
|
||||
}
|
||||
|
||||
_walkVisitedScratch.Reset();
|
||||
_frameWalk.WalkFrame(
|
||||
ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext, _walkVisitedScratch);
|
||||
}
|
||||
|
||||
// #124: look-in cells need prepared shell batches + their statics routed
|
||||
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
|
||||
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never
|
||||
|
|
@ -194,7 +287,7 @@ public sealed class RetailPViewRenderer
|
|||
// seals, the outside-stage predicate, and the frame result.
|
||||
var prepareCells = drawableCells;
|
||||
_lookInCellIds.Clear();
|
||||
if (_lookInFrames.Count > 0)
|
||||
if (_lookInFrames.Count > 0 || walkActive)
|
||||
{
|
||||
_lookInPrepareScratch.Clear();
|
||||
_lookInPrepareScratch.UnionWith(drawableCells);
|
||||
|
|
@ -206,6 +299,13 @@ public sealed class RetailPViewRenderer
|
|||
_lookInCellIds.Add(c);
|
||||
}
|
||||
}
|
||||
if (walkActive)
|
||||
{
|
||||
// The walk's own flood/look-in cell set — unioned in (never
|
||||
// aliased with drawableCells, which the outside-stage and seal
|
||||
// predicates below still need scoped to the OLD flood only).
|
||||
_lookInPrepareScratch.UnionWith(_walkVisitedScratch.Cells);
|
||||
}
|
||||
prepareCells = _lookInPrepareScratch;
|
||||
}
|
||||
|
||||
|
|
@ -330,52 +430,85 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
}
|
||||
|
||||
DrawLandscapeThroughOutsideView(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
partition,
|
||||
viewcone,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE
|
||||
// its portal-only far-Z pass. In retail's strict far→near walk
|
||||
// everything queued at that instant is FARTHER than the structure
|
||||
// being punched, so no already-drained poly can meet a punched
|
||||
// aperture's falsified depth, and everything drained later is
|
||||
// NEARER than the punched structure and legitimately composites in
|
||||
// front of it. The batched outdoor frame reproduces that invariant
|
||||
// here: drain the far prefix — every entry at or beyond the
|
||||
// nearest cell whose exit-portal mask is about to punch far-Z —
|
||||
// against still-true landscape depth. Without this, an exterior
|
||||
// waterfall beyond the cathedral drains after the punches and
|
||||
// z-passes across every aperture pixel whose true depth the punch
|
||||
// erased (#132 regression found at the 2026-08-29 cathedral gate).
|
||||
// Interior roots keep their pre-clear stage-boundary drain.
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
if (walkActive)
|
||||
{
|
||||
passes.FlushLandscapeAlphaFartherThan(
|
||||
ExitPortalMaskBarrierDistance(
|
||||
pvFrame,
|
||||
drawableCells,
|
||||
ctx.Cells,
|
||||
ctx.CameraWorldPosition));
|
||||
// Campaign FW3.2b-2: the walk owns every static draw —
|
||||
// terrain/sky, building shells + punches + look-in cell
|
||||
// statics, and the interior root's own flood shells +
|
||||
// statics (with the depth clear + exit seals at their real
|
||||
// retail turn). The OLD visibility (pvFrame/clipAssembly/
|
||||
// viewcone, already built above) keeps running unchanged to
|
||||
// feed the surviving dynamic routes only (plan §FW3 item 1's
|
||||
// dual-compute split).
|
||||
DrawWalkDrivenStatics(
|
||||
ctx,
|
||||
walkExecutor!,
|
||||
clipAssembly,
|
||||
pvFrame,
|
||||
drawableCells,
|
||||
walkContext!,
|
||||
walkCameraCell,
|
||||
walkLandscape!);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawLandscapeDynamicsPhase(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
partition,
|
||||
viewcone,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawLandscapeThroughOutsideView(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
partition,
|
||||
viewcone,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE
|
||||
// its portal-only far-Z pass. In retail's strict far→near walk
|
||||
// everything queued at that instant is FARTHER than the structure
|
||||
// being punched, so no already-drained poly can meet a punched
|
||||
// aperture's falsified depth, and everything drained later is
|
||||
// NEARER than the punched structure and legitimately composites in
|
||||
// front of it. The batched outdoor frame reproduces that invariant
|
||||
// here: drain the far prefix — every entry at or beyond the
|
||||
// nearest cell whose exit-portal mask is about to punch far-Z —
|
||||
// against still-true landscape depth. Without this, an exterior
|
||||
// waterfall beyond the cathedral drains after the punches and
|
||||
// z-passes across every aperture pixel whose true depth the punch
|
||||
// erased (#132 regression found at the 2026-08-29 cathedral gate).
|
||||
// Interior roots keep their pre-clear stage-boundary drain.
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
passes.FlushLandscapeAlphaFartherThan(
|
||||
ExitPortalMaskBarrierDistance(
|
||||
pvFrame,
|
||||
drawableCells,
|
||||
ctx.Cells,
|
||||
ctx.CameraWorldPosition));
|
||||
}
|
||||
|
||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(passes, pvFrame);
|
||||
DrawCellObjectLists(
|
||||
ctx,
|
||||
passes,
|
||||
pvFrame,
|
||||
clipAssembly,
|
||||
drawableCells,
|
||||
partition,
|
||||
viewcone,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
}
|
||||
|
||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(passes, pvFrame);
|
||||
DrawCellObjectLists(
|
||||
ctx,
|
||||
passes,
|
||||
pvFrame,
|
||||
clipAssembly,
|
||||
drawableCells,
|
||||
partition,
|
||||
viewcone,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
DrawDynamicsLast(
|
||||
ctx,
|
||||
passes,
|
||||
|
|
@ -930,8 +1063,8 @@ public sealed class RetailPViewRenderer
|
|||
return;
|
||||
}
|
||||
}
|
||||
var sink = new WalkShadowSink();
|
||||
_walkShadowFrameWalk.WalkFrame(
|
||||
var sink = new WalkVisitedSetCollector();
|
||||
_frameWalk.WalkFrame(
|
||||
ctx.ViewerCellId, cameraCell, landscape, context, sink);
|
||||
|
||||
int onlyWalk = 0;
|
||||
|
|
@ -956,13 +1089,40 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
}
|
||||
|
||||
private readonly Walk.RetailFrameWalk _walkShadowFrameWalk = new();
|
||||
// Campaign FW3.2b-2: the one RetailFrameWalk instance shared by the
|
||||
// diagnostic shadow probe, the production pre-walk collection pass, and
|
||||
// the real driven run — WalkFrame calls are never concurrent/re-entrant
|
||||
// within a single-threaded render loop, so one shared instance is safe
|
||||
// and avoids re-allocating the walk's own PView scratch per call site.
|
||||
private readonly Walk.RetailFrameWalk _frameWalk = new();
|
||||
|
||||
private sealed class WalkShadowSink : Walk.IWalkEventSink
|
||||
// Campaign FW3.2b-2: the pre-walk collection pass's reusable sink (see
|
||||
// DrawInside's walkActive block) — reset and re-driven once per frame.
|
||||
private readonly WalkVisitedSetCollector _walkVisitedScratch = new();
|
||||
|
||||
/// <summary>Campaign FW3.2b-2 (the I5 dual-shadow pattern, extended for
|
||||
/// production use): an events-only <see cref="Walk.IWalkEventSink"/> that
|
||||
/// collects the SETS a driven run would touch, without doing any leaf
|
||||
/// drawing itself — the shadow probe's original role, now ALSO the
|
||||
/// pre-walk collection pass's role (plan §FW3 item 1: the walk's flood
|
||||
/// cell set for the <c>prepareCells</c> union, the visited building list
|
||||
/// and landscape-cell turn ids for re-sourcing particle owners once the
|
||||
/// walk owns the static routes those owners used to ride).</summary>
|
||||
private sealed class WalkVisitedSetCollector : Walk.IWalkEventSink
|
||||
{
|
||||
public readonly HashSet<uint> Cells = new();
|
||||
public int BuildingCount;
|
||||
public int LandscapeCount;
|
||||
public readonly List<Walk.WalkBuilding> Buildings = new();
|
||||
public readonly HashSet<uint> LandscapeCellIds = new();
|
||||
|
||||
public int BuildingCount => Buildings.Count;
|
||||
public int LandscapeCount => LandscapeCellIds.Count;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Cells.Clear();
|
||||
Buildings.Clear();
|
||||
LandscapeCellIds.Clear();
|
||||
}
|
||||
|
||||
public void Emit(in Walk.WalkEvent walkEvent)
|
||||
{
|
||||
|
|
@ -975,14 +1135,358 @@ public sealed class RetailPViewRenderer
|
|||
foreach (uint id in walkEvent.Cells)
|
||||
Cells.Add(id);
|
||||
break;
|
||||
case Walk.WalkEventKind.Building:
|
||||
BuildingCount++;
|
||||
break;
|
||||
case Walk.WalkEventKind.Landscape:
|
||||
LandscapeCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnLandscapeCellTurn(uint cellId) => LandscapeCellIds.Add(cellId);
|
||||
|
||||
public void OnBuildingTurn(Walk.WalkBuilding building) => Buildings.Add(building);
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2 — THE PRODUCTION ROOTING. Runs the real
|
||||
/// <see cref="Walk.RetailFrameWalk"/> through
|
||||
/// <see cref="Walk.WalkFrameDriver"/> over
|
||||
/// <see cref="WbDrawDispatcher.SubmitOrderedStream"/>: terrain/sky, every
|
||||
/// building's shell + punch + look-in cell statics, and the interior
|
||||
/// root's own flood shells + statics (with retail's depth-clear/exit-seal
|
||||
/// turn, via <see cref="DrawExitPortalMasks"/> bound as the driver's own
|
||||
/// seal action) all draw here, in walk order — replacing
|
||||
/// <see cref="DrawLandscapeThroughOutsideView"/>'s static half,
|
||||
/// <see cref="DrawExitPortalMasks"/>'s old top-level call,
|
||||
/// <see cref="DrawEnvCellShells"/>, and <see cref="DrawCellObjectLists"/>'s
|
||||
/// static half for this frame (plan §FW3 "FW3.2b-2 — the production
|
||||
/// rooting", item 2). Also re-sources the particle owners the routes it
|
||||
/// just replaced used to ride (item 4).</summary>
|
||||
private void DrawWalkDrivenStatics(
|
||||
RetailPViewFrameInput ctx,
|
||||
RetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
PortalVisibilityFrame pvFrame,
|
||||
HashSet<uint> drawableCells,
|
||||
Walk.WalkProductionFrameContext walkContext,
|
||||
Walk.WalkCell? cameraCell,
|
||||
Walk.WalkLandscape landscape)
|
||||
{
|
||||
_walkWorldData!.BeginFrame(_sceneFrameProduct!.SceneQuery, ctx.PlayerLandblockId ?? 0u);
|
||||
|
||||
Action clearInteriorDepth = () =>
|
||||
{
|
||||
// Retail PView::DrawCells 0x005A4872 drains the landscape alpha
|
||||
// list immediately before the gated full depth clear — mirrors
|
||||
// DrawLandscapeThroughOutsideView's own pre-clear drain (this
|
||||
// action only ever fires for an INTERIOR root; see
|
||||
// IWalkFrameLeafRenderer.ClearInteriorDepth's own doc comment for
|
||||
// why the driver never invokes it outdoors).
|
||||
passes.FlushLandscapeAlpha();
|
||||
passes.ClearInteriorDepth();
|
||||
};
|
||||
Action drawExitSeals = () =>
|
||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||
|
||||
var leafRenderer = new WalkProductionLeafRenderer(
|
||||
passes, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
|
||||
var driver = new Walk.WalkFrameDriver(passes.Dispatcher, leafRenderer, _walkWorldData);
|
||||
|
||||
var (frame, encoder) = passes.RequireWalkSubmission();
|
||||
|
||||
int activeTerrainSliceCount = clipAssembly.OutsideViewSlices.Length;
|
||||
if (ctx.RootCell.IsOutdoorNode && activeTerrainSliceCount != 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"walk static cutover: an outdoor root's clip assembly produced "
|
||||
+ $"{activeTerrainSliceCount} outside-view slices, not the expected 1 — "
|
||||
+ "WalkFrameDriver's landscape turn assumes the outdoor root's default "
|
||||
+ "full-screen view (plan §FW3 item 2c's pinned assumption; assert "
|
||||
+ "rather than silently coercing to 1).");
|
||||
}
|
||||
|
||||
driver.RunFrame(
|
||||
_frameWalk,
|
||||
ctx.ViewerCellId,
|
||||
cameraCell,
|
||||
landscape,
|
||||
walkContext,
|
||||
frame,
|
||||
encoder,
|
||||
ctx.ViewProjection,
|
||||
ctx.CameraWorldPosition,
|
||||
activeTerrainSliceCount);
|
||||
|
||||
// Landscape-stage particle owners: the union of every outdoor-static
|
||||
// record from a landscape cell the walk visited this frame, plus
|
||||
// every shell record from a building the walk visited (plan §FW3
|
||||
// item 4 — retail gates particles per cell turn, ShouldDrawParticles
|
||||
// @0x0050FE60, so this is MORE retail-faithful than the old per-
|
||||
// slice sphere filter it replaces).
|
||||
_staticParticleUnionScratch.Clear();
|
||||
foreach (uint cellId in _walkVisitedScratch.LandscapeCellIds)
|
||||
UnionRecordOwners(_walkWorldData.GetOutdoorStatics(cellId), _staticParticleUnionScratch);
|
||||
foreach (Walk.WalkBuilding building in _walkVisitedScratch.Buildings)
|
||||
UnionRecordOwners(_walkWorldData.GetBuildingShellStatics(building), _staticParticleUnionScratch);
|
||||
if (_staticParticleUnionScratch.Count > 0)
|
||||
{
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
ctx,
|
||||
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
|
||||
_staticParticleUnionScratch.Clear();
|
||||
}
|
||||
|
||||
// Cell-stage particle owners for the interior root's OWN flood cells
|
||||
// (non-look-in — the walk draws their statics too, via
|
||||
// OnInteriorFloodDrawTurn/EmitCellTurn, so the retired CellStatic
|
||||
// route's cell-particle submission needs the same re-sourcing).
|
||||
// Look-in cells get their OWN per-cell union in
|
||||
// DrawBuildingLookInDynamics so a static owner is never submitted
|
||||
// twice.
|
||||
_cellParticleOwnerScratch.Clear();
|
||||
foreach (uint cellId in _walkVisitedScratch.Cells)
|
||||
{
|
||||
if (_lookInCellIds.Contains(cellId))
|
||||
continue;
|
||||
UnionRecordOwners(_walkWorldData.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
||||
}
|
||||
if (_cellParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
passes.DrawCellParticles(
|
||||
ctx,
|
||||
new RetailPViewCellSliceContext(0u, NoClipSlice, _cellParticleOwnerScratch));
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnionRecordOwners(
|
||||
Walk.WalkFrameStaticRecords records, HashSet<uint> destination)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId != 0)
|
||||
destination.Add(record.Source.LocalEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
||||
/// <see cref="DrawLandscapeThroughOutsideView"/> + <see cref="DrawBuildingLookIns"/>
|
||||
/// split — the walk now draws every STATIC route (see
|
||||
/// <see cref="DrawWalkDrivenStatics"/>); this method keeps ONLY what
|
||||
/// stays on the OLD visibility pipeline per the plan's dual-compute
|
||||
/// split: outdoor-cell unattached particles, LookInObject dynamics + their
|
||||
/// per-cell particles, the late per-slice outside-dynamics/weather loop,
|
||||
/// and the late particle union submission.</summary>
|
||||
private void DrawLandscapeDynamicsPhase(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
ViewconeCuller viewcone,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
{
|
||||
if (clipAssembly.OutsideViewSlices.Length == 0)
|
||||
return;
|
||||
|
||||
// Ownerless OUTDOOR-cell emitters — now unconditional: the old
|
||||
// hasBuildingLookIns gate only existed to sequence this submission
|
||||
// around the OLD static barrier drains, which no longer run here
|
||||
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
||||
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
||||
|
||||
DrawBuildingLookInDynamics(
|
||||
ctx, passes, clipAssembly, partition, frameEntityPasses, in frameView);
|
||||
|
||||
// LATE phase (per slice): outside-stage dynamics' meshes + weather —
|
||||
// unchanged from DrawLandscapeThroughOutsideView's own late loop.
|
||||
_staticParticleUnionScratch.Clear();
|
||||
int probeSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
passes.SetTerrainClip(slice.Planes);
|
||||
passes.ClearClipRouting();
|
||||
|
||||
_outdoorStaticScratch.Clear();
|
||||
_lateParticleOwnerScratch.Clear();
|
||||
foreach (var e in _outsideStageDynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
|
||||
{
|
||||
_outdoorStaticScratch.Add(e);
|
||||
if (!InteriorEntityPartition.IsIndoorCellId(e.ParentCellId))
|
||||
_lateParticleOwnerScratch.Add(e.Id);
|
||||
}
|
||||
}
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Union(
|
||||
_lateParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
|
||||
probeSliceIndex,
|
||||
0);
|
||||
}
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LandscapeOutsideDynamic,
|
||||
probeSliceIndex,
|
||||
0,
|
||||
_outdoorStaticScratch);
|
||||
RenderFrameEntityDrawRequest? entityDraw =
|
||||
frameEntityPasses is null
|
||||
? null
|
||||
: new RenderFrameEntityDrawRequest(
|
||||
frameView,
|
||||
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
|
||||
probeSliceIndex,
|
||||
0,
|
||||
ctx.PlayerLandblockId ?? 0);
|
||||
probeSliceIndex++;
|
||||
_staticParticleUnionScratch.UnionWith(_lateParticleOwnerScratch);
|
||||
passes.DrawLandscapeSliceLate(
|
||||
ctx,
|
||||
new RetailPViewLandscapeLateSliceContext(slice, _outdoorStaticScratch)
|
||||
{
|
||||
EntityDraw = entityDraw,
|
||||
});
|
||||
}
|
||||
|
||||
// Late-particle union submission — DynamicLast owners excluded, same
|
||||
// as DrawLandscapeThroughOutsideView's own final submission.
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.ExceptRoute(
|
||||
_staticParticleUnionScratch, in frameView, RenderFrameCandidateRoute.DynamicLast);
|
||||
}
|
||||
if (_staticParticleUnionScratch.Count > 0)
|
||||
{
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
ctx,
|
||||
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
|
||||
_staticParticleUnionScratch.Clear();
|
||||
}
|
||||
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
||||
/// <see cref="DrawBuildingLookIns"/> — punches, shells, and look-in cell
|
||||
/// STATICS are now walk-owned (<see cref="Walk.WalkFrameDriver"/>'s
|
||||
/// Building/BuildingShell/LookInStatic turns); this method keeps ONLY the
|
||||
/// LookInObject route (now dynamic-classified — see
|
||||
/// <c>RenderScenePViewFrameBuilder.BuildLookInRoutes</c>) and the per-cell
|
||||
/// particle union that route's owners feed, unioned with the walk's
|
||||
/// static owners for that SAME cell (plan §FW3 item 4 — GetCellStatics
|
||||
/// fills the gap the retired CellStatic-route particle submission left
|
||||
/// for look-in cells specifically).</summary>
|
||||
private void DrawBuildingLookInDynamics(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
{
|
||||
if (_lookInFrames.Count == 0)
|
||||
return;
|
||||
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
var clipKey = new LookInClipCell(frameIndex, cellId);
|
||||
if (!clipAssembly.LookInCellToViewSlices.TryGetValue(
|
||||
clipKey,
|
||||
out ClipViewSlice[]? cellSlices)
|
||||
|| cellSlices.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_cellStaticScratch.Clear();
|
||||
if (partition is not null)
|
||||
{
|
||||
foreach (var e in partition.Dynamics)
|
||||
if (e.ParentCellId == cellId)
|
||||
_cellStaticScratch.Add(e);
|
||||
}
|
||||
|
||||
bool cellDrewObjects = false;
|
||||
_cellParticleUnionScratch.Clear();
|
||||
foreach (ClipViewSlice slice in cellSlices)
|
||||
{
|
||||
int routeIndex = lookInRouteIndex++;
|
||||
passes.UseCellPortalViewRouting(cellId, slice);
|
||||
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_cellParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceOwnerIds(
|
||||
_cellParticleOwnerScratch,
|
||||
_cellStaticScratch);
|
||||
}
|
||||
|
||||
if (frameEntityPasses is not null
|
||||
|| _cellStaticScratch.Count > 0)
|
||||
{
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_cellStaticScratch);
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
DrawEntityRouteOrLegacy(
|
||||
ctx,
|
||||
passes,
|
||||
frameEntityPasses,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_cellStaticScratch,
|
||||
_oneCell);
|
||||
|
||||
cellDrewObjects = true;
|
||||
_cellParticleUnionScratch.UnionWith(
|
||||
_cellParticleOwnerScratch);
|
||||
}
|
||||
}
|
||||
|
||||
// The walk already drew this cell's STATIC content
|
||||
// (WalkFrameDriver's LookInStatic turn) but never submits
|
||||
// particles for it — GetCellStatics fills that gap, unioned
|
||||
// with the dynamic route's own owners so ONE
|
||||
// DrawCellParticles call covers both.
|
||||
if (_walkWorldData is not null)
|
||||
{
|
||||
Walk.WalkFrameStaticRecords statics =
|
||||
_walkWorldData.GetCellStatics(cellId);
|
||||
foreach (RenderProjectionRecord record in statics.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId != 0)
|
||||
{
|
||||
_cellParticleUnionScratch.Add(record.Source.LocalEntityId);
|
||||
cellDrewObjects = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cellDrewObjects)
|
||||
{
|
||||
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
||||
cellId, NoClipSlice, _cellParticleUnionScratch));
|
||||
}
|
||||
}
|
||||
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLandscapeThroughOutsideView(
|
||||
|
|
|
|||
|
|
@ -122,6 +122,11 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
{
|
||||
private readonly RenderSceneShadowRuntime _shadow;
|
||||
private readonly CurrentRenderSceneOracle? _current;
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the frame's retained-scene read view for
|
||||
/// the walk's world-data provider (the same query
|
||||
/// <see cref="BuildAndBorrow"/> reads its routes from).</summary>
|
||||
internal RenderSceneQuery SceneQuery => _shadow.Query;
|
||||
private readonly WbDrawDispatcher? _dispatcher;
|
||||
private readonly RenderScenePViewFrameBuilder _builder = new();
|
||||
private readonly RenderFrameExchange _exchange = new();
|
||||
|
|
@ -1123,7 +1128,6 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
private const byte EnvCellProjectionDomain = 2;
|
||||
|
||||
private readonly HashSet<RenderProjectionId> _projectionIds = [];
|
||||
private RenderProjectionRecord[] _outdoor = [];
|
||||
private RenderProjectionRecord[] _dynamics = [];
|
||||
private RenderProjectionRecord[] _cell = [];
|
||||
private RenderProjectionRecord[] _dirty = [];
|
||||
|
|
@ -1134,14 +1138,9 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
// marks every non-player part): an object whose cell drew with a look-in
|
||||
// must not enter the final dynamics route again.
|
||||
private readonly HashSet<uint> _lookInCellScratch = new();
|
||||
private RenderProjectionRecord[] _cellRoute = [];
|
||||
private readonly Dictionary<RenderProjectionId, int>
|
||||
_outdoorPositions = [];
|
||||
private readonly Dictionary<RenderProjectionId, int>
|
||||
_dynamicPositions = [];
|
||||
private int _outdoorCount;
|
||||
private int _dynamicCount;
|
||||
private int _cellRouteCount;
|
||||
private int _dirtyCount;
|
||||
private RenderSceneGeneration _indexGeneration;
|
||||
private ulong _indexRevision;
|
||||
|
|
@ -1164,7 +1163,13 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
_projectionIds.Clear();
|
||||
LoadSceneIndices(input.Scene);
|
||||
|
||||
BuildOutdoorRoutes(writer, in input);
|
||||
// Campaign FW3.2b-2: LandscapeOutdoorStatic, LandscapeBuildingShell,
|
||||
// and CellStatic no longer emit here — WalkFrameDriver draws every
|
||||
// outdoor static, building shell, and cell static (including
|
||||
// look-in cell statics) directly through OrderedDrawStream (plan
|
||||
// §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject
|
||||
// keeps emitting, but BuildLookInRoutes below is now filtered to
|
||||
// DYNAMIC candidates only — the walk owns that route's statics.
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0;
|
||||
frameIndex < input.LookInFrames.Count;
|
||||
|
|
@ -1175,13 +1180,8 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
in input,
|
||||
frameIndex,
|
||||
ref lookInRouteIndex);
|
||||
BuildLookInBuildingShellRoutes(
|
||||
writer,
|
||||
in input,
|
||||
frameIndex);
|
||||
}
|
||||
BuildOutsideDynamicRoutes(writer, in input);
|
||||
BuildCellStaticRoute(writer, in input);
|
||||
BuildDynamicLastRoute(writer, in input);
|
||||
writer.Publish();
|
||||
AcknowledgeCachedDirtyRecords();
|
||||
|
|
@ -1200,18 +1200,6 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
if (scene.Generation != _indexGeneration
|
||||
|| revision != _indexRevision)
|
||||
{
|
||||
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
|
||||
_outdoorCount = scene.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorStatic,
|
||||
_outdoor);
|
||||
_outdoorCount = CompactAndSort(
|
||||
_outdoor,
|
||||
_outdoorCount);
|
||||
BuildPositionIndex(
|
||||
_outdoor,
|
||||
_outdoorCount,
|
||||
_outdoorPositions);
|
||||
|
||||
EnsureCapacity(ref _dynamics, counts.Dynamic);
|
||||
_dynamicCount = scene.CopyIndexTo(
|
||||
RenderSceneIndex.Dynamic,
|
||||
|
|
@ -1235,12 +1223,6 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
for (int index = 0; index < _dirtyCount; index++)
|
||||
{
|
||||
RenderProjectionRecord record = _dirty[index];
|
||||
if (_outdoorPositions.TryGetValue(
|
||||
record.Id,
|
||||
out int outdoorPosition))
|
||||
{
|
||||
_outdoor[outdoorPosition] = record;
|
||||
}
|
||||
if (_dynamicPositions.TryGetValue(
|
||||
record.Id,
|
||||
out int dynamicPosition))
|
||||
|
|
@ -1255,16 +1237,6 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
for (int index = 0; index < _dirtyCount; index++)
|
||||
{
|
||||
RenderProjectionId id = _dirty[index].Id;
|
||||
if (_outdoorPositions.TryGetValue(
|
||||
id,
|
||||
out int outdoorPosition))
|
||||
{
|
||||
_outdoor[outdoorPosition] =
|
||||
_outdoor[outdoorPosition] with
|
||||
{
|
||||
DirtyMask = RenderDirtyMask.None,
|
||||
};
|
||||
}
|
||||
if (_dynamicPositions.TryGetValue(
|
||||
id,
|
||||
out int dynamicPosition))
|
||||
|
|
@ -1289,103 +1261,13 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
positions.Add(records[index].Id, index);
|
||||
}
|
||||
|
||||
private void BuildOutdoorRoutes(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input)
|
||||
{
|
||||
int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
|
||||
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
|
||||
{
|
||||
int count = 0;
|
||||
EnsureCapacity(ref _survivors, _outdoorCount);
|
||||
for (int i = 0; i < _outdoorCount; i++)
|
||||
{
|
||||
RenderProjectionRecord record = _outdoor[i];
|
||||
if (record.EntityPayload.IsBuildingShell
|
||||
&& RetailPViewRenderer.FindLookInFrameIndex(
|
||||
record.Source.BuildingShellAnchorCellId,
|
||||
input.LookInFrames,
|
||||
input.Cells) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sphere(in record, out Vector3 center, out float radius);
|
||||
if (!input.Viewcone.SphereVisibleInOutsideSlice(
|
||||
sliceIndex,
|
||||
in center,
|
||||
radius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_survivors[count++] = record;
|
||||
writer.AddOutdoor(in record);
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
sliceIndex,
|
||||
0,
|
||||
_survivors.AsSpan(0, count));
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLookInBuildingShellRoutes(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input,
|
||||
int frameIndex)
|
||||
{
|
||||
int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
|
||||
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
|
||||
{
|
||||
int count = 0;
|
||||
EnsureCapacity(ref _survivors, _outdoorCount);
|
||||
for (int i = 0; i < _outdoorCount; i++)
|
||||
{
|
||||
RenderProjectionRecord record = _outdoor[i];
|
||||
if (!record.EntityPayload.IsBuildingShell
|
||||
|| RetailPViewRenderer.FindLookInFrameIndex(
|
||||
record.Source.BuildingShellAnchorCellId,
|
||||
input.LookInFrames,
|
||||
input.Cells) != frameIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Sphere(in record, out Vector3 center, out float radius);
|
||||
if (!input.Viewcone.SphereVisibleInOutsideSlice(
|
||||
sliceIndex,
|
||||
in center,
|
||||
radius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_survivors[count++] = record;
|
||||
writer.AddOutdoor(in record);
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
int routeIndex =
|
||||
RetailPViewRenderer.LookInBuildingShellRouteIndex(
|
||||
frameIndex,
|
||||
sliceCount,
|
||||
sliceIndex);
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LandscapeBuildingShell,
|
||||
routeIndex,
|
||||
0,
|
||||
_survivors.AsSpan(0, count));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: <c>LookInObject</c> now carries DYNAMIC
|
||||
/// candidates only — the walk draws every look-in cell's STATIC content
|
||||
/// directly (<c>WalkFrameDriver</c>'s <c>LookInStatic</c> turn, via
|
||||
/// <c>WalkProductionWorldData.GetCellStatics</c>), so loading this
|
||||
/// route's cell contents with <c>includeStatics: false</c> is what keeps
|
||||
/// the two draws from doubling a look-in room's furniture (plan §FW3
|
||||
/// item 3).</summary>
|
||||
private void BuildLookInRoutes(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input,
|
||||
|
|
@ -1408,6 +1290,7 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
int count = LoadCell(
|
||||
input.Scene,
|
||||
cellId,
|
||||
includeStatics: false,
|
||||
includeDynamics: true);
|
||||
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
|
||||
{
|
||||
|
|
@ -1475,61 +1358,6 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
}
|
||||
}
|
||||
|
||||
private void BuildCellStaticRoute(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input)
|
||||
{
|
||||
_cellRouteCount = 0;
|
||||
IReadOnlyList<uint> ordered = input.PortalFrame.OrderedVisibleCells;
|
||||
for (int i = ordered.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = ordered[i];
|
||||
if (!input.DrawableCells.Contains(cellId))
|
||||
continue;
|
||||
|
||||
int loaded = LoadCell(
|
||||
input.Scene,
|
||||
cellId,
|
||||
includeDynamics: false);
|
||||
int count = 0;
|
||||
for (int index = 0; index < loaded; index++)
|
||||
{
|
||||
RenderProjectionRecord record = _cell[index];
|
||||
Sphere(in record, out Vector3 center, out float radius);
|
||||
if (!input.Viewcone.SphereVisibleInCell(
|
||||
cellId,
|
||||
in center,
|
||||
radius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_cell[count++] = record;
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
writer.AddCellRange(
|
||||
cellId,
|
||||
i,
|
||||
_cell.AsSpan(0, count));
|
||||
EnsureCapacity(
|
||||
ref _cellRoute,
|
||||
checked(_cellRouteCount + count));
|
||||
_cell.AsSpan(0, count).CopyTo(
|
||||
_cellRoute.AsSpan(_cellRouteCount));
|
||||
_cellRouteCount += count;
|
||||
}
|
||||
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.CellStatic,
|
||||
0,
|
||||
0,
|
||||
_cellRoute.AsSpan(0, _cellRouteCount));
|
||||
}
|
||||
|
||||
private void BuildDynamicLastRoute(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input)
|
||||
|
|
@ -1593,18 +1421,25 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
private int LoadCell(
|
||||
RenderSceneQuery scene,
|
||||
uint cellId,
|
||||
bool includeDynamics)
|
||||
bool includeDynamics,
|
||||
bool includeStatics = true)
|
||||
{
|
||||
int staticCount = scene.GetCellStaticCount(cellId);
|
||||
int staticCount = includeStatics
|
||||
? scene.GetCellStaticCount(cellId)
|
||||
: 0;
|
||||
int dynamicCount = includeDynamics
|
||||
? scene.GetCellDynamicCount(cellId)
|
||||
: 0;
|
||||
EnsureCapacity(
|
||||
ref _cell,
|
||||
checked(staticCount + dynamicCount));
|
||||
int count = scene.CopyCellStaticsTo(
|
||||
cellId,
|
||||
_cell.AsSpan(0, staticCount));
|
||||
int count = 0;
|
||||
if (staticCount > 0)
|
||||
{
|
||||
count = scene.CopyCellStaticsTo(
|
||||
cellId,
|
||||
_cell.AsSpan(0, staticCount));
|
||||
}
|
||||
if (dynamicCount > 0)
|
||||
{
|
||||
count += scene.CopyCellDynamicsTo(
|
||||
|
|
|
|||
180
src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs
Normal file
180
src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
|
||||
namespace AcDream.App.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.2b-2: the production <see cref="IWalkFrameWorldData"/> over
|
||||
/// the retained scene (<see cref="RenderSceneQuery"/>) and the FW3.1
|
||||
/// <see cref="WalkBuildingRegistry"/>. Rebuilt facts per frame via
|
||||
/// <see cref="BeginFrame"/>:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>Cell statics — <see cref="RenderSceneQuery.CopyCellStaticsTo"/> on
|
||||
/// demand, one pooled array per distinct cell per frame (a cell can be
|
||||
/// visited once by the root flood OR once per admitting look-in portal; the
|
||||
/// per-frame cache keeps the copy single).</item>
|
||||
/// <item>Outdoor statics — ONE <see cref="RenderSceneQuery.CopyIndexTo"/>
|
||||
/// sweep bucketed by landscape cell id
|
||||
/// (<c>(lb & 0xFFFF0000) | (cellX*8 + cellY + 1)</c> from the record's
|
||||
/// world position — the same encoding <see cref="IWalkEventSink.OnLandscapeCellTurn"/>
|
||||
/// computes), EXCLUDING building shells (they draw at their building's own
|
||||
/// shell turn, retail <c>CPhysicsPart::Draw(parts, 0)</c> @0x0059f331, not
|
||||
/// at the cell's <c>DrawObjCell</c> turn).</item>
|
||||
/// <item>Building shells — the same sweep's <c>IsBuildingShell</c> records
|
||||
/// bucketed by <c>Source.BuildingShellAnchorCellId</c>; a
|
||||
/// <see cref="WalkBuilding"/> maps to its anchor via its first
|
||||
/// non-exit portal's destination (the SAME rule
|
||||
/// <c>LandblockLoader</c> used to author the anchor).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// The tuple landblock id handed to the classifier is the frame's player
|
||||
/// landblock — the packed path's own convention for every
|
||||
/// <c>RenderFrameEntityDrawRequest</c>.
|
||||
/// </summary>
|
||||
internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
||||
{
|
||||
private readonly WalkBuildingRegistry _buildings;
|
||||
private RenderSceneQuery _scene;
|
||||
private uint _tupleLandblockId;
|
||||
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorByCell = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _shellMaterialized = new();
|
||||
private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
|
||||
private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256];
|
||||
|
||||
internal WalkProductionWorldData(WalkBuildingRegistry buildings)
|
||||
{
|
||||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||||
}
|
||||
|
||||
/// <summary>Rebuilds the frame's outdoor/shell buckets and clears the
|
||||
/// per-cell cache. Call once per frame before the driver runs.</summary>
|
||||
internal void BeginFrame(RenderSceneQuery scene, uint tupleLandblockId)
|
||||
{
|
||||
_scene = scene;
|
||||
_tupleLandblockId = tupleLandblockId;
|
||||
_cellCache.Clear();
|
||||
_outdoorMaterialized.Clear();
|
||||
_shellMaterialized.Clear();
|
||||
foreach (List<RenderProjectionRecord> bucket in _outdoorByCell.Values)
|
||||
bucket.Clear();
|
||||
foreach (List<RenderProjectionRecord> bucket in _shellsByAnchor.Values)
|
||||
bucket.Clear();
|
||||
|
||||
int count;
|
||||
while (true)
|
||||
{
|
||||
count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch);
|
||||
if (count < _sweepScratch.Length)
|
||||
break;
|
||||
_sweepScratch = new RenderProjectionRecord[_sweepScratch.Length * 2];
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ref readonly RenderProjectionRecord record = ref _sweepScratch[i];
|
||||
if (record.EntityPayload.IsBuildingShell)
|
||||
{
|
||||
uint anchor = record.Source.BuildingShellAnchorCellId;
|
||||
if (!_shellsByAnchor.TryGetValue(anchor, out List<RenderProjectionRecord>? shells))
|
||||
_shellsByAnchor[anchor] = shells = new List<RenderProjectionRecord>();
|
||||
shells.Add(record);
|
||||
continue;
|
||||
}
|
||||
uint cellId = LandscapeCellId(record.Transform.Position);
|
||||
if (!_outdoorByCell.TryGetValue(cellId, out List<RenderProjectionRecord>? bucket))
|
||||
_outdoorByCell[cellId] = bucket = new List<RenderProjectionRecord>();
|
||||
bucket.Add(record);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The landscape cell owning a world position — retail's
|
||||
/// 24 m cell grid inside the 192 m landblock, the same
|
||||
/// <c>(lb & 0xFFFF0000) | (cellX*8 + cellY + 1)</c> encoding the walk's
|
||||
/// landscape turn emits.</summary>
|
||||
internal static uint LandscapeCellId(Vector3 worldPosition)
|
||||
{
|
||||
int lbX = (int)MathF.Floor(worldPosition.X / 192f);
|
||||
int lbY = (int)MathF.Floor(worldPosition.Y / 192f);
|
||||
float localX = worldPosition.X - lbX * 192f;
|
||||
float localY = worldPosition.Y - lbY * 192f;
|
||||
int cellX = Math.Clamp((int)(localX / 24f), 0, 7);
|
||||
int cellY = Math.Clamp((int)(localY / 24f), 0, 7);
|
||||
uint landblock = ((uint)(byte)lbX << 24) | ((uint)(byte)lbY << 16);
|
||||
return landblock | (uint)(cellX * 8 + cellY + 1);
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetCellStatics(uint cellId)
|
||||
{
|
||||
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
int count;
|
||||
while (true)
|
||||
{
|
||||
count = _scene.CopyCellStaticsTo(cellId, _cellScratch);
|
||||
if (count < _cellScratch.Length)
|
||||
break;
|
||||
_cellScratch = new RenderProjectionRecord[_cellScratch.Length * 2];
|
||||
}
|
||||
WalkFrameStaticRecords records = count == 0
|
||||
? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
|
||||
: new WalkFrameStaticRecords(_cellScratch[..count], _tupleLandblockId);
|
||||
_cellCache[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId)
|
||||
{
|
||||
if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
WalkFrameStaticRecords records =
|
||||
_outdoorByCell.TryGetValue(cellId, out List<RenderProjectionRecord>? bucket)
|
||||
&& bucket.Count > 0
|
||||
? new WalkFrameStaticRecords([.. bucket], _tupleLandblockId)
|
||||
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
|
||||
_outdoorMaterialized[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
|
||||
{
|
||||
uint anchor = AnchorCellId(building);
|
||||
if (anchor == 0)
|
||||
return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
|
||||
if (_shellMaterialized.TryGetValue(anchor, out WalkFrameStaticRecords cached))
|
||||
return cached;
|
||||
WalkFrameStaticRecords records =
|
||||
_shellsByAnchor.TryGetValue(anchor, out List<RenderProjectionRecord>? shells)
|
||||
&& shells.Count > 0
|
||||
? new WalkFrameStaticRecords([.. shells], _tupleLandblockId)
|
||||
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
|
||||
_shellMaterialized[anchor] = records;
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <summary>The building's authored shell anchor: its first non-exit
|
||||
/// portal's destination cell — the SAME rule <c>LandblockLoader</c> used
|
||||
/// when it stamped <c>BuildingShellAnchorCellId</c> on the shell entity.</summary>
|
||||
internal static uint AnchorCellId(WalkBuilding building)
|
||||
{
|
||||
foreach (ref readonly WalkBldPortal portal in building.Portals.AsSpan())
|
||||
{
|
||||
if (portal.OtherCellId != 0xFFFFFFFFu)
|
||||
return portal.OtherCellId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building)
|
||||
{
|
||||
if (!_buildings.TryGetEntry(building, out WalkBuildingFactory.Entry? entry))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"walk building 0x{building.PositionCellId:X8} has no committed registry entry");
|
||||
}
|
||||
return entry.WorldTransform;
|
||||
}
|
||||
}
|
||||
|
|
@ -210,6 +210,32 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.2b-2: the production frame/encoder pair for
|
||||
/// <see cref="WalkFrameDriver"/>'s own <see cref="SubmitOrderedStream"/>
|
||||
/// calls (contrast this stage's diagnostic-target callers, which supply
|
||||
/// their own frame/encoder — see <see cref="SubmitOrderedStream"/>'s own
|
||||
/// doc comment). Reads the SAME world-pass scope <see cref="SubmitRhi"/>
|
||||
/// already requires (<see cref="RequireRhiFrame"/> /
|
||||
/// <c>_scope.RequireEncoder()</c>) — fails loud rather than handing the
|
||||
/// driver a null pair when the world phase is not bracketing.
|
||||
/// </summary>
|
||||
internal (IGpuFrame Frame, IGpuPassEncoder Encoder) RequireWalkSubmission() =>
|
||||
(RequireRhiFrame(), _scope!.RequireEncoder());
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.2b-2: the live colour-attachment size, for
|
||||
/// <see cref="WalkProductionFrameContext"/>'s viewport (the walk's ray
|
||||
/// caster needs the REAL viewport, not the FW0/FW1 capture-client
|
||||
/// fixture constants — see that class's own doc comment). Null outside
|
||||
/// the world phase (no scope published yet); the caller falls back to
|
||||
/// the fixture constants with a comment in that case rather than
|
||||
/// failing loud, since a missing scope here is a startup-ordering
|
||||
/// timing question, not a misconfiguration.
|
||||
/// </summary>
|
||||
internal (int Width, int Height)? WalkAttachmentExtent =>
|
||||
_scope is null ? null : (_scope.AttachmentWidth, _scope.AttachmentHeight);
|
||||
|
||||
/// <summary>
|
||||
/// Submits <paramref name="stream"/> in walk order through the existing
|
||||
/// RHI: per-instance-first emission (see the type doc comment), one
|
||||
|
|
|
|||
|
|
@ -14,326 +14,33 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
private static readonly RenderSceneGeneration Generation =
|
||||
RenderSceneGeneration.FromRaw(1);
|
||||
|
||||
[Fact]
|
||||
public void Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners()
|
||||
{
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord outdoor = Record(
|
||||
0x0100_0000_0000_0001,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord buildingShell = Record(
|
||||
0x0100_0000_0000_0008,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true);
|
||||
RenderProjectionRecord hidden = Record(
|
||||
0x0100_0000_0000_0002,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
flags: RenderProjectionFlags.Hidden
|
||||
| RenderProjectionFlags.SpatiallyResident);
|
||||
RenderProjectionRecord withdrawn = Record(
|
||||
0x0100_0000_0000_0003,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
flags: RenderProjectionFlags.Draw);
|
||||
RenderProjectionRecord shell = Record(
|
||||
0x0200_0000_0000_0004,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord cellStatic = Record(
|
||||
0x0100_0000_0000_0005,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord outdoorDynamic = Record(
|
||||
0x0300_0000_0000_0006,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
RenderProjectionRecord cellDynamic = Record(
|
||||
0x0300_0000_0000_0007,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord[] records =
|
||||
[
|
||||
outdoor,
|
||||
buildingShell,
|
||||
hidden,
|
||||
withdrawn,
|
||||
shell,
|
||||
cellStatic,
|
||||
outdoorDynamic,
|
||||
cellDynamic,
|
||||
];
|
||||
var deltas = new RenderProjectionDelta[records.Length];
|
||||
for (int index = 0; index < records.Length; index++)
|
||||
{
|
||||
deltas[index] = RenderProjectionDelta.Register(
|
||||
Generation,
|
||||
(ulong)index + 1,
|
||||
records[index]);
|
||||
}
|
||||
scene.Apply(deltas);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
RenderSceneDigest digest =
|
||||
scene.BuildDigest(new RenderSceneDigestBuffer());
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene.OpenQuery(),
|
||||
digest,
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
[],
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[outdoor.Id, hidden.Id, buildingShell.Id],
|
||||
view.OutdoorStaticCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(
|
||||
[cellStatic.Id],
|
||||
view.CellStaticCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(
|
||||
[outdoorDynamic.Id, cellDynamic.Id],
|
||||
view.DynamicCandidates.ToArray()
|
||||
.Select(static item => item.Id));
|
||||
Assert.Equal(6, view.Transforms.Length);
|
||||
Assert.Equal(6, view.RouteCandidates.Length);
|
||||
Assert.Equal(3, view.RouteRanges.Length);
|
||||
RenderFrameCandidateRange outdoorRange = Assert.Single(
|
||||
view.RouteRanges.ToArray(),
|
||||
range => range.Route
|
||||
== RenderFrameCandidateRoute.LandscapeOutdoorStatic);
|
||||
Assert.Equal(3, outdoorRange.Count);
|
||||
Assert.Contains(
|
||||
view.RouteCandidates.Slice(
|
||||
outdoorRange.Offset,
|
||||
outdoorRange.Count).ToArray(),
|
||||
item => item.Id == buildingShell.Id);
|
||||
Assert.DoesNotContain(
|
||||
view.RouteRanges.ToArray(),
|
||||
range => range.Route
|
||||
== RenderFrameCandidateRoute.LandscapeBuildingShell);
|
||||
Assert.DoesNotContain(
|
||||
view.RouteCandidates.ToArray(),
|
||||
item => item.Id == withdrawn.Id || item.Id == shell.Id);
|
||||
Assert.Equal(
|
||||
new RenderFrameDiagnosticCounts(
|
||||
OutdoorStaticCandidates: 3,
|
||||
CellStaticCandidates: 1,
|
||||
DynamicCandidates: 2,
|
||||
TransformCount: 6,
|
||||
OpaqueClassificationCount: 0,
|
||||
AlphaClassificationCount: 0,
|
||||
LightSetCount: 0,
|
||||
SelectionPartCount: 0,
|
||||
RouteCandidateCount: 6,
|
||||
EntityCandidateCount: 6,
|
||||
MeshPartCount: 6),
|
||||
view.DiagnosticCounts);
|
||||
Assert.Equal(6, view.EntityCandidates.Length);
|
||||
Assert.Equal(6, view.MeshParts.Length);
|
||||
Assert.Same(portal, view.PortalFrame);
|
||||
Assert.Same(clip, view.ClipAssembly);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_OrdersMultiSliceBuildingShellsAfterLookIns()
|
||||
{
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord outdoor = Record(
|
||||
0x0100_0000_0000_0101,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionRecord buildingShell = Record(
|
||||
0x0100_0000_0000_0102,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: Cell);
|
||||
RenderProjectionRecord lookInObject = Record(
|
||||
0x0100_0000_0000_0103,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, outdoor),
|
||||
RenderProjectionDelta.Register(Generation, 2, buildingShell),
|
||||
RenderProjectionDelta.Register(Generation, 3, lookInObject),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
PortalVisibilityFrame lookIn = Portal(Cell);
|
||||
lookIn.SourceBuildingKey = 2u;
|
||||
lookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
|
||||
var anchorCell = new LoadedCell
|
||||
{
|
||||
CellId = Cell,
|
||||
BuildingId = 2u,
|
||||
};
|
||||
ClipFrameAssembly clip = TwoSliceClip(Cell);
|
||||
clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
|
||||
[clip.CellIdToViewSlices[Cell][0]];
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
RenderSceneDigest digest =
|
||||
scene.BuildDigest(new RenderSceneDigestBuffer());
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene.OpenQuery(),
|
||||
digest,
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[lookIn],
|
||||
[Cell],
|
||||
new DictionaryCellSource(anchorCell),
|
||||
[],
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[
|
||||
(RenderFrameCandidateRoute.LandscapeOutdoorStatic, 0, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeOutdoorStatic, 1, 0u),
|
||||
(RenderFrameCandidateRoute.LookInObject, 0, Cell),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
|
||||
(RenderFrameCandidateRoute.CellStatic, 0, 0u),
|
||||
],
|
||||
view.RouteRanges.ToArray().Select(static range =>
|
||||
(range.Route, range.RouteIndex, range.CellId)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes()
|
||||
{
|
||||
const uint secondCell = 0xA9B40180u;
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord firstShell = Record(
|
||||
0x0100_0000_0000_0201,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: Cell);
|
||||
RenderProjectionRecord secondShell = Record(
|
||||
0x0100_0000_0000_0202,
|
||||
RenderProjectionClass.OutdoorStatic,
|
||||
isBuildingShell: true,
|
||||
buildingShellAnchorCellId: secondCell);
|
||||
RenderProjectionRecord firstObject = Record(
|
||||
0x0100_0000_0000_0203,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord secondObject = Record(
|
||||
0x0100_0000_0000_0204,
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
parentCell: secondCell);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, firstShell),
|
||||
RenderProjectionDelta.Register(Generation, 2, secondShell),
|
||||
RenderProjectionDelta.Register(Generation, 3, firstObject),
|
||||
RenderProjectionDelta.Register(Generation, 4, secondObject),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
PortalVisibilityFrame firstLookIn = Portal(Cell);
|
||||
firstLookIn.SourceBuildingKey = 2u;
|
||||
firstLookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
|
||||
PortalVisibilityFrame secondLookIn = Portal(secondCell);
|
||||
secondLookIn.SourceBuildingKey = 3u;
|
||||
secondLookIn.SourceBuildingLandblockId = secondCell & 0xFFFF0000u;
|
||||
var firstAnchor = new LoadedCell
|
||||
{
|
||||
CellId = Cell,
|
||||
BuildingId = 2u,
|
||||
};
|
||||
var secondAnchor = new LoadedCell
|
||||
{
|
||||
CellId = secondCell,
|
||||
BuildingId = 3u,
|
||||
};
|
||||
ClipFrameAssembly clip = TwoSliceClip(Cell);
|
||||
clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
|
||||
[clip.CellIdToViewSlices[Cell][0]];
|
||||
clip.LookInCellToViewSlices[new LookInClipCell(1, secondCell)] =
|
||||
[clip.CellIdToViewSlices[Cell][0]];
|
||||
ViewconeCuller viewcone =
|
||||
ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
RenderSceneDigest digest =
|
||||
scene.BuildDigest(new RenderSceneDigestBuffer());
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene.OpenQuery(),
|
||||
digest,
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
[firstLookIn, secondLookIn],
|
||||
[Cell],
|
||||
new DictionaryCellSource(firstAnchor, secondAnchor),
|
||||
[],
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
Assert.Equal(
|
||||
[
|
||||
(RenderFrameCandidateRoute.LookInObject, 0, Cell),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
|
||||
(RenderFrameCandidateRoute.LookInObject, 1, secondCell),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 2, 0u),
|
||||
(RenderFrameCandidateRoute.LandscapeBuildingShell, 3, 0u),
|
||||
],
|
||||
view.RouteRanges.ToArray()
|
||||
.Where(static range =>
|
||||
range.Route is RenderFrameCandidateRoute.LookInObject
|
||||
or RenderFrameCandidateRoute.LandscapeBuildingShell)
|
||||
.Select(static range =>
|
||||
(range.Route, range.RouteIndex, range.CellId)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
// Campaign FW3.2b-2 (adjudicated 2026-08-30): Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners,
|
||||
// Builder_OrdersMultiSliceBuildingShellsAfterLookIns, and
|
||||
// Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes are
|
||||
// DELETED — all three asserted route-range interleaving for
|
||||
// LandscapeOutdoorStatic/LandscapeBuildingShell/CellStatic, which
|
||||
// RenderScenePViewFrameBuilder no longer emits at all (WalkFrameDriver
|
||||
// draws every outdoor static, building shell, and cell static — look-in
|
||||
// included — directly through OrderedDrawStream; plan §FW3
|
||||
// "FW3.2b-2 — the production rooting", item 3). There is no successor
|
||||
// assertion to write: the routes themselves are retired, not relocated.
|
||||
|
||||
[Fact]
|
||||
public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords()
|
||||
{
|
||||
// Campaign FW3.2b-2 (adjudicated 2026-08-30): re-expressed against
|
||||
// the DYNAMIC index (LiveDynamicRoot / DynamicCandidates) instead of
|
||||
// the now-dead OutdoorStatic index — LoadSceneIndices' dirty-record
|
||||
// refresh + sort-key reorder mechanism this test proves is SHARED
|
||||
// between the two indices; the OutdoorStatic side was deleted along
|
||||
// with BuildOutdoorRoutes (its only reader).
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord later = Record(
|
||||
0x0100_0000_0000_0012,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
RenderProjectionRecord earlier = Record(
|
||||
0x0100_0000_0000_0011,
|
||||
RenderProjectionClass.OutdoorStatic);
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, later),
|
||||
|
|
@ -353,7 +60,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
{
|
||||
Assert.Equal(
|
||||
[earlier.Id, later.Id],
|
||||
first.OutdoorStaticCandidates.ToArray()
|
||||
first.DynamicCandidates.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
}
|
||||
finally
|
||||
|
|
@ -401,7 +108,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
projected.Transform.LocalToWorld);
|
||||
Assert.Equal(
|
||||
[earlier.Id, later.Id],
|
||||
second.OutdoorStaticCandidates.ToArray()
|
||||
second.DynamicCandidates.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
}
|
||||
finally
|
||||
|
|
@ -431,7 +138,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
{
|
||||
Assert.Equal(
|
||||
[later.Id, earlier.Id],
|
||||
third.OutdoorStaticCandidates.ToArray()
|
||||
third.DynamicCandidates.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
}
|
||||
finally
|
||||
|
|
@ -463,14 +170,18 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[Fact]
|
||||
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
|
||||
{
|
||||
WorldEntity outdoor = Entity(10, serverGuid: 0, parentCell: null);
|
||||
WorldEntity cellStatic = Entity(11, serverGuid: 0, parentCell: Cell);
|
||||
// Campaign FW3.2b-2 (adjudicated 2026-08-30): no static entities here
|
||||
// — LandscapeOutdoorStatic/CellStatic no longer flow through the
|
||||
// packed product at all (WalkFrameDriver owns every static now), so
|
||||
// a static observed on the LEGACY oracle side with nothing on the
|
||||
// packed side would report a permanent mismatch unrelated to what
|
||||
// this test is proving (withdrawal detection on the SURVIVING
|
||||
// DynamicLast route).
|
||||
WorldEntity outdoorDynamic =
|
||||
Entity(1_000_012, 0x8000_0012, parentCell: null);
|
||||
WorldEntity cellDynamic =
|
||||
Entity(1_000_013, 0x8000_0013, parentCell: Cell);
|
||||
WorldEntity[] entities =
|
||||
[outdoor, cellStatic, outdoorDynamic, cellDynamic];
|
||||
WorldEntity[] entities = [outdoorDynamic, cellDynamic];
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var partition = new InteriorEntityPartition.Result();
|
||||
InteriorEntityPartition.Partition(
|
||||
|
|
@ -479,16 +190,6 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[Entry(entities)],
|
||||
oracle);
|
||||
oracle.BeginPViewFrame();
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LandscapeOutdoorStatic,
|
||||
0,
|
||||
0,
|
||||
[outdoor]);
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.CellStatic,
|
||||
0,
|
||||
0,
|
||||
[cellStatic]);
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.DynamicLast,
|
||||
0,
|
||||
|
|
@ -496,14 +197,6 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[outdoorDynamic, cellDynamic]);
|
||||
oracle.CompletePViewFrame();
|
||||
oracle.BeginDispatcherFrame();
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 1,
|
||||
[(outdoor, 0, Landblock)]);
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 1,
|
||||
[(cellStatic, 0, Landblock)]);
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 2,
|
||||
|
|
@ -544,15 +237,15 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
Assert.True(matched.Enabled);
|
||||
Assert.Equal(1uL, matched.ComparisonCount);
|
||||
Assert.Equal(1uL, matched.SuccessfulComparisonCount);
|
||||
Assert.Equal(4, matched.ExpectedCandidateCount);
|
||||
Assert.Equal(4, matched.ActualCandidateCount);
|
||||
Assert.Equal(2, matched.ExpectedCandidateCount);
|
||||
Assert.Equal(2, matched.ActualCandidateCount);
|
||||
Assert.Equal(0, matched.MismatchCount);
|
||||
Assert.Null(matched.FirstMismatch);
|
||||
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
|
||||
Assert.Equal(1uL, matched.PackedInputComparisonCount);
|
||||
Assert.Equal(1uL, matched.PackedInputSuccessfulComparisonCount);
|
||||
Assert.Equal(4, matched.PackedInputExpectedCount);
|
||||
Assert.Equal(4, matched.PackedInputActualCount);
|
||||
Assert.Equal(2, matched.PackedInputExpectedCount);
|
||||
Assert.Equal(2, matched.PackedInputActualCount);
|
||||
Assert.Equal(0, matched.PackedInputMismatchCount);
|
||||
Assert.Null(matched.PackedInputFirstMismatch);
|
||||
Assert.Equal(
|
||||
|
|
@ -581,10 +274,10 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
Assert.Equal(2uL, mismatch.ComparisonCount);
|
||||
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
|
||||
Assert.Equal(1, mismatch.MismatchCount);
|
||||
Assert.Contains("field=count expected=4 actual=3", mismatch.FirstMismatch);
|
||||
Assert.Contains("field=count expected=2 actual=1", mismatch.FirstMismatch);
|
||||
Assert.Equal(1, mismatch.PackedInputMismatchCount);
|
||||
Assert.Contains(
|
||||
"field=count expected=4 actual=3",
|
||||
"field=count expected=2 actual=1",
|
||||
mismatch.PackedInputFirstMismatch);
|
||||
Assert.Equal(2, logs.Count);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,10 +196,15 @@ public sealed class RetailPViewPassExecutorTests
|
|||
[Fact]
|
||||
public void DrawInside_production_product_supplies_the_ordered_entity_routes()
|
||||
{
|
||||
// Campaign FW3.2b-2: no cell-static entity here (adjudicated
|
||||
// 2026-08-30) — CellStatic no longer flows through the packed
|
||||
// product at all (WalkFrameDriver owns every cell static now, via
|
||||
// OrderedDrawStream, not this route); a static entity registered on
|
||||
// the LEGACY side with nothing on the packed side would report a
|
||||
// permanent MismatchCount, which is not what this test is proving.
|
||||
// The route-consumption-order assertion below still exercises the
|
||||
// one route that DID survive the cutover (DynamicLast).
|
||||
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
||||
WorldEntity cellStatic = Entity(
|
||||
30u,
|
||||
parentCellId: interior.CellId);
|
||||
WorldEntity dynamic = Entity(
|
||||
31u,
|
||||
serverGuid: 0x80000031u,
|
||||
|
|
@ -208,17 +213,6 @@ public sealed class RetailPViewPassExecutorTests
|
|||
var oracle = new CurrentRenderSceneOracle();
|
||||
using var shadow = new RenderSceneShadowRuntime(
|
||||
RenderSceneGeneration.FromRaw(1));
|
||||
RenderProjectionRecord staticRecord =
|
||||
RenderProjectionRecordFactory.ProjectEntity(
|
||||
StaticRenderProjectionJournal.StaticEntityId(
|
||||
landblockId,
|
||||
cellStatic.Id),
|
||||
RenderProjectionClass.IndoorCellStatic,
|
||||
RenderOwnerIncarnation.FromRaw(cellStatic.Id),
|
||||
landblockId,
|
||||
interior.CellId,
|
||||
cellStatic,
|
||||
spatiallyVisible: true);
|
||||
RenderProjectionRecord dynamicRecord =
|
||||
RenderProjectionRecordFactory.ProjectEntity(
|
||||
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
|
||||
|
|
@ -228,7 +222,6 @@ public sealed class RetailPViewPassExecutorTests
|
|||
interior.CellId,
|
||||
dynamic,
|
||||
spatiallyVisible: true);
|
||||
shadow.Journal.Register(in staticRecord);
|
||||
shadow.Journal.Register(in dynamicRecord);
|
||||
shadow.DrainUpdateBoundary();
|
||||
var product = new RenderScenePViewFrameProductController(
|
||||
|
|
@ -238,13 +231,12 @@ public sealed class RetailPViewPassExecutorTests
|
|||
using var executor = new RecordingExecutor();
|
||||
|
||||
renderer.DrawInside(
|
||||
Frame(interior, [cellStatic, dynamic]),
|
||||
Frame(interior, [dynamic]),
|
||||
executor);
|
||||
|
||||
AssertAppearsInOrder(
|
||||
string.Join('|', executor.Operations),
|
||||
"entity-frame-begin",
|
||||
"entity-route:CellStatic:0:00000000",
|
||||
"entity-route:DynamicLast:0:00000000",
|
||||
"entity-frame-complete");
|
||||
Assert.Equal(0, product.Snapshot.MismatchCount);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue