Campaign OVERHAUL S3 chunk 4 fix round 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10.4), on top of 6ba4b0b87. K1, K2, K3, K4, K6, K7, K8 (K5 was the lead's own plan-doc note, already done). K1 (blocking) — the Assert.Single pin over DrawLandscapeDynamicsPhase's DrawWeatherOnce call site stayed green even wrapped in a foreach (it counts distinct call-site offsets, not loop shape). Added a real LOOP-SHAPE pin reusing CompiledCallGraph.ReadBranches: asserts no backward branch (target offset < its own offset) spans the DrawWeatherOnce call's IL offset — the shape every C# for/foreach/while loop compiles to. K2 (minor) — the Collect-time "OC" print fired on ctx.WeatherGateOpen alone while the Replay-time draw independently re-derived clipAssembly.OutsideViewSlices.Length > 0, which could diverge on an interior root whose landscape turn ran but whose reassembled outside-view slices ended up empty. WalkFrameDriver now owns one WeatherTurnFired flag, set unconditionally inside OnWeatherTurn (so it tracks "did the print's own gate hold" regardless of the transcript flag) and reset every BeginFrame/AbortFrame; RetailPViewRenderer.DrawLandscapeDynamicsPhase gates DrawWeatherOnce on it instead of re-deriving its own condition. K3 (minor) — ClipFrame.SetTerrainClip (the terrain OutsideView writer) had no remaining caller after chunk 4's original round and K4 below; deleted along with IWorldPassSurface.SetTerrainClip, RhiWorldPassSurface.SetTerrainClip, the now-orphaned PublishTerrainClip helper, and PrepareClipFrame's terrain-clip publish call. The terrain/sky shaders still declare the TerrainClip UBO (verified: terrain_modern.vert, terrain_atmospheric.vert, sky.vert all read uTerrainClipCount/uTerrainClipPlanes), so the section binding itself (WorldFrameSectionBinding.BindTerrainClip) stays — its existing zeroed-ring fallback (for when nothing published the section) now binds that same all-zero disabled block on every frame, identical bytes to the old permanent NoClip/Reset default. K4 (major) — DrawWalkSky still looped the OUTDOOR case once per active landscape view under a doorway scissor + BindTerrainClip + EnableClipDistances (the INTERIOR case already drew once unclipped, FW4 slice 6). Retail draws GameSky::Draw(sky,0) ONCE, unconditionally, before LScape::draw's block loop, for both root kinds. Collapsed DrawWalkSky to one unconditional, unclipped draw; deleted BeginDoorwayScissor and the RetailPViewPassExecutor.EnableClipDistances wrapper (both lost their only caller). _surface.BeginScissor/EndScissor and IWorldPassSurface.EnableClipDistances stay: RhiWorldPassSurface.ClearInteriorDepth still ends an active scissor, and WorldScenePassExecutor (the separate flat-world path) still calls EnableClipDistances directly. K6 (minor) — the punch-fan CPU/GPU equivalence pin hand-built a ClipViewSlice from ClipPlaneSet.From's raw output, which could pass even if ClipFrameAssembler.Assemble's own packing/array-construction diverged from that output. Rewrote it to build a real PortalVisibilityFrame and run it through ClipFrameAssembler.Assemble, reading the planes back through assembly.OutsideViewSlices[0].Planes — the exact outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes)) path ReassembleOutsideViewFromWalk (the walk's real interior-root producer) shares. K7 (note) — restored the "a real look-in slice never reuses the reserved no-clip slot 0" assertion the deleted VisibleClipSlotsInLookInTurn used to prove, via a new internal test-only accessor (WalkFrameDriver.LookInSliceClipSlotAt) reading the same _lookInSlices storage InteriorFloodViewClipPlanesAt resolves through _clipFrame.GetSlotPlanes(slice.ClipSlot). K8 (note) — documented in DrawWeatherOnce's own comment that the weather mesh drawing before the rain particles is this method's own call-order choice; retail's single GameSky::Draw(sky,1) imposes no order between acdream's two substitutes. Every new/changed pin's mutation was hand-verified this session (temporarily applied, ran the specific test, confirmed the exact failing assertion, then reverted — see the parent task's structured report for the four failing-assertion texts). No register row added or removed — every change here deletes an acdream-only rule or repairs a pin; none introduces a new deviation. Full solution build: 0 warnings/0 errors. App hermetic (Lane!=InstalledDat/PreparedPackage/Live/ Manual/Timing/Windows/Linux/SystemFont & Purpose!=Diagnostic & Status!=KnownFailure): 6831/6831. InstalledDat: 244 pass/1 skip/4 known — identical to 6ba4b0b87's own baseline (2x #383 layout tests, TowerAscent, and the pre-existing #458 WalkLandscape.CheckBlocks block-visibility divergence, unrelated to and untouched by this round). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
875 lines
42 KiB
C#
875 lines
42 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// App-layer port of the retail indoor render orchestration:
|
|
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
|
|
/// PView::DrawInside -> ConstructView -> DrawCells.
|
|
/// </summary>
|
|
internal sealed class RetailPViewRenderer
|
|
{
|
|
private readonly RenderSceneShadowRuntime _renderSceneShadow;
|
|
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
|
private readonly RetailPViewFrameResult _frameResultScratch = new();
|
|
|
|
// MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across
|
|
// frames instead of `new HashSet<uint>(pvFrame.OrderedVisibleCells)` every
|
|
// call. Every walk consumer reads it synchronously in this frame.
|
|
private readonly HashSet<uint> _drawableCellsScratch = new();
|
|
private readonly HashSet<uint> _visibleCellsScratch = new();
|
|
|
|
// FW3 visual-gate fix: the interior root's dynamics phase, invoked by
|
|
// the driver's clearInteriorDepth closure at the walk's pre-clear
|
|
// boundary (retail draws outside objects inside LScape::draw, before
|
|
// the clear+seals). Assigned per frame around the Replay, always
|
|
// cleared in finally.
|
|
private Action? _walkPreClearDynamics;
|
|
|
|
// Output-only Facility Hub staircase probe. This counter is consulted
|
|
// only while ACDREAM_PROBE_FACILITY_STAIRS=1 and never affects the walk.
|
|
private ulong _probeFacilityStairFrame;
|
|
private string? _probeFacilityStairRootSignature;
|
|
private string? _probeCathedralStairRootSignature;
|
|
|
|
// FW6 allocation closeout: the walk's large event/view/route scratch,
|
|
// frame context, and one-cell leaf collections are renderer-lifetime
|
|
// owners. Only their frame-local bindings change. Before this cutover all
|
|
// three were constructed inside DrawInside and accounted for the measured
|
|
// ~1.5 MiB/frame dense-town tail.
|
|
private Walk.WalkFrameDriver? _walkFrameDriverScratch;
|
|
private Walk.WalkProductionFrameContext? _walkFrameContextScratch;
|
|
private WalkProductionLeafRenderer? _walkLeafRendererScratch;
|
|
private readonly Action _walkFlushLandscapeAction;
|
|
private readonly Action _walkClearInteriorDepthAction;
|
|
private readonly Func<int> _walkDrawExitSealsFunc;
|
|
private RetailPViewPassExecutor? _activeWalkPasses;
|
|
private RetailPViewFrameInput? _activeWalkFrame;
|
|
private ClipFrameAssembly? _activeWalkClipAssembly;
|
|
|
|
// Campaign FW3.2b-2: the walk's production world-data registries
|
|
// (published/retired by LandblockRenderPublisher) plus the per-frame
|
|
// driver state. Null until the composition passes them; the static
|
|
// cutover requires all three.
|
|
private readonly Walk.WalkBuildingRegistry _walkBuildings;
|
|
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(
|
|
RenderSceneShadowRuntime renderSceneShadow,
|
|
Walk.WalkBuildingRegistry walkBuildings,
|
|
Walk.WalkLandscapeAssembler walkLandscape,
|
|
CellVisibility walkCellRegistry,
|
|
ShadowObjectRegistry shadows)
|
|
{
|
|
_renderSceneShadow = renderSceneShadow
|
|
?? throw new ArgumentNullException(nameof(renderSceneShadow));
|
|
_walkBuildings = walkBuildings
|
|
?? throw new ArgumentNullException(nameof(walkBuildings));
|
|
_walkLandscape = walkLandscape
|
|
?? throw new ArgumentNullException(nameof(walkLandscape));
|
|
_walkCellRegistry = walkCellRegistry
|
|
?? throw new ArgumentNullException(nameof(walkCellRegistry));
|
|
_walkWorldData = new Walk.WalkProductionWorldData(
|
|
_walkBuildings,
|
|
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
|
_walkFlushLandscapeAction = FlushWalkLandscape;
|
|
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
|
_walkDrawExitSealsFunc = DrawWalkExitSeals;
|
|
}
|
|
|
|
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
|
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
|
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
|
// caller's per-building frustum pre-gate on aperture bounds (GameWindow's
|
|
// gather); seeds themselves are unbounded.
|
|
private const float OutdoorBuildingSeedDistance = float.PositiveInfinity;
|
|
|
|
internal RetailPViewFrameResult DrawInside(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(ctx);
|
|
ArgumentNullException.ThrowIfNull(passes);
|
|
passes.BeginFrame();
|
|
// Campaign FW4: there is one production renderer and one visibility
|
|
// owner. The fake/legacy executor path was useful during the cutover,
|
|
// but retaining it here kept the retired PortalVisibilityBuilder and
|
|
// look-in seed machinery alive. Fail loudly if composition ever tries
|
|
// to reintroduce that split.
|
|
RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
|
|
?? throw new InvalidOperationException(
|
|
"The retail frame walk requires RetailPViewPassExecutor.");
|
|
ClipFrameAssembly clipAssembly = passes.BeginWalkClipFrame(
|
|
ctx.RootCell.IsOutdoorNode,
|
|
_clipAssemblyScratch);
|
|
// FW4 slice 1: PrepareClipFrame (the one clip-region publication)
|
|
// moved BELOW the walk block — an interior-rooted walk frame
|
|
// re-derives the outside-view slices from the walk's own views
|
|
// first, so the appended slots join the same single publication.
|
|
|
|
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
|
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
|
|
// so every visible cell's shell has a prepared batch and seals — killing the grey
|
|
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
|
|
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
|
|
_drawableCellsScratch.Clear();
|
|
HashSet<uint> drawableCells = _drawableCellsScratch;
|
|
|
|
// Campaign FW3.4a: THE ONE WALK. Builds walkContext/walkLandscape/
|
|
// walkCameraCell exactly as the pre-FW3.4a pre-walk collection pass
|
|
// did, then drives WalkFrameDriver.Collect — a SINGLE RetailFrameWalk
|
|
// pass that both learns the flood/visited-cell set (needed below,
|
|
// BEFORE prepareCells is finalized, so EnvCellRenderer prepares
|
|
// batches for every shell the driver will draw later in this same
|
|
// DrawInside call) and records the walk's draw events for Replay
|
|
// further down, in DrawWalkDrivenStatics. The former SECOND walk pass
|
|
// (a dedicated set-collecting sink, run again through this same
|
|
// driver machinery just to submit) is gone — see WalkFrameDriver's
|
|
// own doc comment for the FW3.4 perf numbers that motivated this.
|
|
// _walkWorldData.BeginFrame precedes Collect deliberately: Collect's
|
|
// stream appends classify records immediately (WalkStaticStreamPopulator
|
|
// runs at append time, not at Replay time), so the world data must
|
|
// already be rebuilt for this frame before the walk starts.
|
|
Walk.WalkFrameDriver walkDriver;
|
|
{
|
|
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;
|
|
// S3 chunk 4 (O3): the pre-computed weather gate — the SAME
|
|
// predicate RetailPViewPassExecutor.DrawWeatherOnce checks at
|
|
// Replay — handed to the walk context so RetailFrameWalk.
|
|
// DrawLandscape can fire IWalkEventSink.OnWeatherTurn at
|
|
// retail's real Collect-time turn without importing App render
|
|
// toggles into the pure decomp port.
|
|
bool weatherGateOpen = RetailPViewPassExecutor.ShouldDrawWeatherOnce(
|
|
ctx.RenderSky, ctx.RenderWeather, ctx.PlayerCellId);
|
|
if (_walkFrameContextScratch is null)
|
|
{
|
|
_walkFrameContextScratch = new Walk.WalkProductionFrameContext(
|
|
_walkCellRegistry,
|
|
_walkBuildings,
|
|
ctx.ViewerEyePos,
|
|
forward,
|
|
ctx.ViewProjection,
|
|
viewportWidth,
|
|
viewportHeight,
|
|
ctx.ViewerCellId,
|
|
weatherGateOpen);
|
|
}
|
|
else
|
|
{
|
|
_walkFrameContextScratch.Reset(
|
|
ctx.ViewerEyePos,
|
|
forward,
|
|
ctx.ViewProjection,
|
|
viewportWidth,
|
|
viewportHeight,
|
|
ctx.ViewerCellId,
|
|
weatherGateOpen);
|
|
}
|
|
Walk.WalkProductionFrameContext walkContext = _walkFrameContextScratch;
|
|
_walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos);
|
|
Walk.WalkLandscape walkLandscape = _walkLandscape.Landscape;
|
|
|
|
Walk.WalkCell? walkCameraCell = null;
|
|
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).");
|
|
}
|
|
}
|
|
|
|
if (ctx.RootCell.IsOutdoorNode && clipAssembly.OutsideViewSlices.Length != 1)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"walk static cutover: an outdoor root's clip assembly produced "
|
|
+ $"{clipAssembly.OutsideViewSlices.Length} outside-view slices, not the "
|
|
+ "expected 1 — the outdoor root draws through the full-screen default "
|
|
+ "view (retail set_default_view; the walk fans exactly its own 1), and "
|
|
+ "the outdoor slice data still comes from the assembler (plan §FW3 item "
|
|
+ "2c's pinned assumption; assert rather than silently coercing to 1).");
|
|
}
|
|
|
|
_walkWorldData!.BeginFrame(
|
|
_renderSceneShadow!.Query,
|
|
ctx.PlayerLandblockId ?? 0u,
|
|
ctx.RenderCenterLbX,
|
|
ctx.RenderCenterLbY);
|
|
|
|
_activeWalkPasses = walkExecutor;
|
|
_activeWalkFrame = ctx;
|
|
_activeWalkClipAssembly = clipAssembly;
|
|
if (_walkLeafRendererScratch is null)
|
|
{
|
|
_walkLeafRendererScratch = new WalkProductionLeafRenderer(
|
|
walkExecutor,
|
|
ctx,
|
|
clipAssembly,
|
|
_walkFlushLandscapeAction,
|
|
_walkClearInteriorDepthAction,
|
|
_walkDrawExitSealsFunc);
|
|
}
|
|
else
|
|
{
|
|
_walkLeafRendererScratch.Reset(
|
|
walkExecutor,
|
|
ctx,
|
|
clipAssembly,
|
|
_walkFlushLandscapeAction,
|
|
_walkClearInteriorDepthAction,
|
|
_walkDrawExitSealsFunc);
|
|
}
|
|
|
|
if (_walkFrameDriverScratch is null)
|
|
{
|
|
_walkFrameDriverScratch = new Walk.WalkFrameDriver(
|
|
walkExecutor.Dispatcher,
|
|
_walkLeafRendererScratch,
|
|
_walkWorldData,
|
|
clipFrame: clipAssembly.Frame);
|
|
}
|
|
else
|
|
{
|
|
_walkFrameDriverScratch.RebindFrame(
|
|
_walkLeafRendererScratch,
|
|
clipAssembly.Frame);
|
|
}
|
|
walkDriver = _walkFrameDriverScratch;
|
|
|
|
try
|
|
{
|
|
walkDriver.Collect(
|
|
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
|
|
ctx.ViewProjection, ctx.CameraWorldPosition);
|
|
}
|
|
catch
|
|
{
|
|
ClearWalkFrameBindings();
|
|
throw;
|
|
}
|
|
// FW4 slice 1: an interior root's terrain/sky/punch clip slices
|
|
// come from THE WALK'S OWN outside_view — retail's one
|
|
// visibility structure. See ClipFrameAssembler.
|
|
// ReassembleOutsideViewFromWalk's doc comment for the boundary-
|
|
// frame desync (the stairwell/grass flash) this retires. The
|
|
// outdoor root keeps the assembler's single full-screen slice
|
|
// (asserted ==1 above; identical content by construction).
|
|
if (walkCameraCell is not null)
|
|
{
|
|
ClipFrameAssembler.ReassembleOutsideViewFromWalk(
|
|
clipAssembly,
|
|
_frameWalk.InteriorOutsideView,
|
|
viewportWidth,
|
|
viewportHeight);
|
|
}
|
|
|
|
// The walk's visited set replaces PortalVisibilityFrame's old
|
|
// OrderedVisibleCells side-channel for every production consumer.
|
|
_drawableCellsScratch.Clear();
|
|
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
|
|
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
|
|
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
|
|
{
|
|
static int TargetMembership(IReadOnlyCollection<uint> cells)
|
|
{
|
|
const uint a = 0x8A02015Eu;
|
|
const uint b = 0x8A02015Fu;
|
|
const uint c = 0x8A0201C1u;
|
|
int mask = 0;
|
|
foreach (uint cell in cells)
|
|
{
|
|
if (cell == a) mask |= 1;
|
|
if (cell == b) mask |= 2;
|
|
if (cell == c) mask |= 4;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
_probeFacilityStairFrame++;
|
|
int floodMask = TargetMembership(walkDriver.InteriorFloodCells);
|
|
int visitedMask = TargetMembership(walkDriver.VisitedCells);
|
|
int turnMask = TargetMembership(walkDriver.LookInCells);
|
|
string signature = $"{ctx.ViewerCellId:X8}:{ctx.PlayerCellId:X8}:"
|
|
+ $"{ctx.RootCell.CellId:X8}:{floodMask}:{visitedMask}:{turnMask}:"
|
|
+ $"{walkDriver.InteriorFloodCells.Count}:{walkDriver.VisitedCells.Count}:"
|
|
+ $"{walkDriver.LookInCellTurns.Count}";
|
|
bool changed = !string.Equals(
|
|
_probeFacilityStairRootSignature,
|
|
signature,
|
|
StringComparison.Ordinal);
|
|
_probeFacilityStairRootSignature = signature;
|
|
if (changed)
|
|
{
|
|
static string DescribeMask(int mask) =>
|
|
$"15e={((mask & 1) != 0 ? 1 : 0)},"
|
|
+ $"15f={((mask & 2) != 0 ? 1 : 0)},"
|
|
+ $"1c1={((mask & 4) != 0 ? 1 : 0)}";
|
|
Console.WriteLine(
|
|
$"[facility-root] f={_probeFacilityStairFrame} changed={(changed ? 1 : 0)} "
|
|
+ $"viewer=0x{ctx.ViewerCellId:X8} player=0x{ctx.PlayerCellId:X8} "
|
|
+ $"root=0x{ctx.RootCell.CellId:X8} res={ctx.CameraCellResolution} "
|
|
+ $"eye=({ctx.ViewerEyePos.X:F4},{ctx.ViewerEyePos.Y:F4},{ctx.ViewerEyePos.Z:F4}) "
|
|
+ $"playerPos=({ctx.PlayerViewPosition.X:F4},{ctx.PlayerViewPosition.Y:F4},{ctx.PlayerViewPosition.Z:F4}) "
|
|
+ $"rootFlood={walkDriver.InteriorFloodCells.Count}"
|
|
+ $"[{DescribeMask(floodMask)}] "
|
|
+ $"visited={walkDriver.VisitedCells.Count}"
|
|
+ $"[{DescribeMask(visitedMask)}] "
|
|
+ $"turns={walkDriver.LookInCellTurns.Count}"
|
|
+ $"[{DescribeMask(turnMask)}]");
|
|
}
|
|
|
|
static int CathedralMembership(IReadOnlyCollection<uint> cells)
|
|
{
|
|
const uint oldBuilding = 0xF4180107u;
|
|
const uint stairParent = 0xF4180112u;
|
|
int mask = 0;
|
|
foreach (uint cell in cells)
|
|
{
|
|
if (cell == oldBuilding) mask |= 1;
|
|
if (cell == stairParent) mask |= 2;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
static uint BuildingAnchor(Walk.WalkBuilding building)
|
|
{
|
|
foreach (Walk.WalkBldPortal portal in building.Portals)
|
|
{
|
|
if (portal.OtherCellId != 0xFFFFFFFFu)
|
|
return portal.OtherCellId;
|
|
}
|
|
return 0u;
|
|
}
|
|
|
|
int cathedralFlood = CathedralMembership(walkDriver.InteriorFloodCells);
|
|
int cathedralVisited = CathedralMembership(walkDriver.VisitedCells);
|
|
int cathedralLookIn = CathedralMembership(walkDriver.LookInCells);
|
|
bool bucket107 = _walkWorldData.StaticBucketContains(
|
|
0xF4180107u,
|
|
0x020009A2u);
|
|
bool bucket112 = _walkWorldData.StaticBucketContains(
|
|
0xF4180112u,
|
|
0x020009A2u);
|
|
string buildingAnchors = string.Join(",", walkDriver.VisitedBuildings
|
|
.ConvertAll(building => $"0x{BuildingAnchor(building):X8}"));
|
|
string cathedralSignature =
|
|
$"{ctx.ViewerCellId:X8}:{ctx.PlayerCellId:X8}:{ctx.RootCell.CellId:X8}:"
|
|
+ $"{cathedralFlood}:{cathedralVisited}:{cathedralLookIn}:"
|
|
+ $"{(bucket107 ? 1 : 0)}:{(bucket112 ? 1 : 0)}:{buildingAnchors}";
|
|
bool cathedralChanged = !string.Equals(
|
|
_probeCathedralStairRootSignature,
|
|
cathedralSignature,
|
|
StringComparison.Ordinal);
|
|
_probeCathedralStairRootSignature = cathedralSignature;
|
|
if (cathedralChanged
|
|
&& ((ctx.ViewerCellId & 0xFFFF0000u) == 0xF4180000u
|
|
|| (ctx.PlayerCellId & 0xFFFF0000u) == 0xF4180000u))
|
|
{
|
|
static string DescribeCathedralMask(int mask) =>
|
|
$"107={((mask & 1) != 0 ? 1 : 0)},"
|
|
+ $"112={((mask & 2) != 0 ? 1 : 0)}";
|
|
Console.WriteLine(
|
|
$"[cathedral-stair] f={_probeFacilityStairFrame} "
|
|
+ $"viewer=0x{ctx.ViewerCellId:X8} player=0x{ctx.PlayerCellId:X8} "
|
|
+ $"root=0x{ctx.RootCell.CellId:X8} res={ctx.CameraCellResolution} "
|
|
+ $"eye=({ctx.ViewerEyePos.X:F4},{ctx.ViewerEyePos.Y:F4},{ctx.ViewerEyePos.Z:F4}) "
|
|
+ $"playerPos=({ctx.PlayerViewPosition.X:F4},{ctx.PlayerViewPosition.Y:F4},{ctx.PlayerViewPosition.Z:F4}) "
|
|
+ $"rootFlood=[{DescribeCathedralMask(cathedralFlood)}] "
|
|
+ $"visited=[{DescribeCathedralMask(cathedralVisited)}] "
|
|
+ $"lookIn=[{DescribeCathedralMask(cathedralLookIn)}] "
|
|
+ $"buckets=[107={(bucket107 ? 1 : 0)},112={(bucket112 ? 1 : 0)}] "
|
|
+ $"buildings=[{buildingAnchors}]");
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// FW4 slice 1: the ONE clip-region publication, after any walk
|
|
// reassembly so the walk-derived outside-view slots are included
|
|
// (moved from directly after AssembleClipFrame; the count is
|
|
// reservation metadata the RHI arm ignores).
|
|
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
|
passes.PrepareClipFrame(terrainUploadCount);
|
|
|
|
// Production prepares exactly the one walk's visited-cell set.
|
|
HashSet<uint> prepareCells = drawableCells;
|
|
|
|
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
|
|
// here was the seam-floor flicker mechanism — retail's visible_cell_table is
|
|
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
|
// is built once per frame in GameWindow, player-anchored.)
|
|
|
|
passes.PrepareCellBatches(ctx, prepareCells);
|
|
|
|
{
|
|
RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts;
|
|
RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts);
|
|
RenderProjectionCounts sourceCounts = retainedCounts;
|
|
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
|
clipAssembly,
|
|
drawableCells,
|
|
_visibleCellsScratch,
|
|
counts,
|
|
sourceCounts,
|
|
diagnosticPartition: null);
|
|
passes.EmitDiagnostics(ctx, result);
|
|
|
|
// The one collected walk owns terrain, cell shells, buildings,
|
|
// statics, dynamics, particles, punches, and alpha barriers at
|
|
// their retail turns. Interior-root landscape services run at the
|
|
// pre-clear callback; outdoor roots have no clear and run after
|
|
// replay.
|
|
_walkPreClearDynamics = () =>
|
|
{
|
|
DrawLandscapeDynamicsPhase(
|
|
ctx,
|
|
passes,
|
|
clipAssembly,
|
|
walkDriver);
|
|
};
|
|
try
|
|
{
|
|
DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!);
|
|
if (ctx.RootCell.IsOutdoorNode)
|
|
{
|
|
DrawLandscapeDynamicsPhase(
|
|
ctx,
|
|
passes,
|
|
clipAssembly,
|
|
walkDriver);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_walkPreClearDynamics = null;
|
|
ClearWalkFrameBindings();
|
|
}
|
|
|
|
// OUTDOOR root: the LScape-boundary alpha drain deferred from the
|
|
// landscape stage runs HERE, after punches, interior shells, cell
|
|
// objects, and the dynamics pass — the frame's complete opaque
|
|
// world. Retail's walk draws all of those before its boundary
|
|
// flush (LScape::draw includes every cell's objects,
|
|
// DrawSortCell 0x005A17C0), so this is the same one-list far→near
|
|
// composite over finished depth; draining at the stage end instead
|
|
// let every later opaque mesh overwrite the flames (#132).
|
|
if (ctx.RootCell.IsOutdoorNode)
|
|
passes.FlushLandscapeAlpha();
|
|
|
|
// Interior-cell UNATTACHED emitters (spell ground effects and
|
|
// swirls anchored in EnvCells) draw in this final world scope —
|
|
// the cells' walls and the seals already own the depth buffer, so
|
|
// one unclipped submission matches retail's cell-walk insertion.
|
|
// Outdoor-cell unattached emitters drew in the landscape stage.
|
|
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: false);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private readonly Walk.RetailFrameWalk _frameWalk = new();
|
|
|
|
/// <summary>S3 chunk 2 (§8.2 B3): the flush half of the former combined
|
|
/// <c>ClearWalkInteriorDepth</c> — retail's <c>D3DPolyRender::FlushAlphaList(0f)</c>
|
|
/// @0x005a4872 plus the pre-clear dynamics phase (FW3 visual-gate fix:
|
|
/// retail draws outside objects inside <c>LScape::draw</c>, before the
|
|
/// clear+seals). Called by the driver's <c>LandscapeFlush</c> leaf,
|
|
/// unconditionally whenever an interior root's own flood has a
|
|
/// surviving exit view (<c>ov>0</c>) — see
|
|
/// <see cref="Walk.IWalkFrameLeafRenderer.FlushLandscape"/>.</summary>
|
|
private void FlushWalkLandscape()
|
|
{
|
|
RetailPViewPassExecutor passes = _activeWalkPasses
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active pass binding.");
|
|
|
|
// FW3 visual-gate fix (owner report: doors/candles invisible looking
|
|
// out; the crossing vanish): retail draws outside objects inside
|
|
// LScape::draw, before the clear+seals.
|
|
_walkPreClearDynamics?.Invoke();
|
|
passes.FlushLandscapeAlpha();
|
|
}
|
|
|
|
/// <summary>S3 chunk 2 (§8.2 B3): the Z-clear half of the former combined
|
|
/// <c>ClearWalkInteriorDepth</c> — <c>PView::DrawCells</c>'s gated full
|
|
/// depth clear (pc:432731-432732). The driver only calls this leaf when
|
|
/// its persistent <c>PortalsDrawnCount</c> was nonzero at the
|
|
/// read-then-zero decision (S3 §8.1 R4) — see
|
|
/// <see cref="Walk.IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
|
|
private void ClearWalkInteriorDepth()
|
|
{
|
|
RetailPViewPassExecutor passes = _activeWalkPasses
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active pass binding.");
|
|
|
|
passes.ClearInteriorDepth();
|
|
}
|
|
|
|
/// <summary>Returns the total exit-seal fan count submitted this turn
|
|
/// (S3 §8.2 B2), so the driver can re-arm its persistent
|
|
/// <c>PortalsDrawnCount</c> for the NEXT <c>ov>0</c> interior-root
|
|
/// flood's clear decision.</summary>
|
|
private int DrawWalkExitSeals()
|
|
{
|
|
RetailPViewFrameInput frame = _activeWalkFrame
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active frame binding.");
|
|
RetailPViewPassExecutor passes = _activeWalkPasses
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active pass binding.");
|
|
Walk.WalkFrameDriver driver = _walkFrameDriverScratch
|
|
?? throw new InvalidOperationException(
|
|
"The retained walk leaf has no active driver binding.");
|
|
|
|
return DrawWalkExitPortalMasks(frame, passes, driver);
|
|
}
|
|
|
|
private void ClearWalkFrameBindings()
|
|
{
|
|
_walkFrameDriverScratch?.AbortFrame();
|
|
_activeWalkPasses = null;
|
|
_activeWalkFrame = null;
|
|
_activeWalkClipAssembly = null;
|
|
}
|
|
|
|
/// <summary>Campaign FW3.2b-2 — THE PRODUCTION ROOTING; Campaign FW3.4a —
|
|
/// REPLAY ONLY. <paramref name="driver"/> already ran its Collect pass
|
|
/// earlier in <see cref="DrawInside"/> (before <c>PrepareCellBatches</c>);
|
|
/// this method's job is now just <see cref="Walk.WalkFrameDriver.Replay"/>
|
|
/// (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 — all 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) plus re-sourcing the particle owners the
|
|
/// routes it replaced used to ride, from the driver's own visited
|
|
/// sets (plan §FW3 "FW3.2b-2 — the production rooting", items 2 and
|
|
/// 4).</summary>
|
|
private void DrawWalkDrivenStatics(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
Walk.WalkFrameDriver driver)
|
|
{
|
|
var (frame, encoder) = passes.RequireWalkSubmission();
|
|
// S3 chunk 3 (§9.2 B3): the whole-stage terrain draw this fed
|
|
// (RetailPViewPassExecutor.DrawWalkTerrainSlice's inViewLandcells
|
|
// filter) is gone — the walk's own per-cell LandCell turns are now
|
|
// the sole terrain-visibility authority, so there is no plumbing
|
|
// left to feed here.
|
|
try
|
|
{
|
|
driver.Replay(frame, encoder);
|
|
}
|
|
finally
|
|
{
|
|
// S3 chunk 3 fix round 1 (F3): "the end of the walk replay" —
|
|
// exactly where the deleted whole-stage terrain leaf's own
|
|
// Begin()/Complete() bracket used to close. Pushes this frame's
|
|
// ONE accumulated terrain-timing sample (every DrawWalkLandCellBatch
|
|
// call since the last CompleteWalkTerrainFrame) and publishes the
|
|
// periodic [TERRAIN-DIAG] line if the cadence is due. In a finally
|
|
// so a throwing Replay cannot leak its ticks into the next frame.
|
|
passes.CompleteWalkTerrainFrame();
|
|
}
|
|
|
|
// Landscape-stage static-owner particles (candles, the cathedral
|
|
// falls) submit AT THEIR OWN WALK TURNS inside Replay
|
|
// (WalkFrameEventKind.StaticParticles) for BOTH root kinds — the
|
|
// #132 positional invariant (e102fb36's user-verified rule ported
|
|
// walk-natively): each owner cell's emitters enter the one alpha
|
|
// queue at that cell's far-to-near position, so every nearer
|
|
// building's pre-punch AlphaBarrier drains the farther content
|
|
// against still-true depth before its punch stamps far-Z.
|
|
|
|
// Campaign OVERHAUL S2 chunk 6: an emitter draws ONCE, at its own
|
|
// cell's turn inside Replay (WalkFrameEventKind.CellParticles fires for
|
|
// every visited interior cell — root flood and look-in alike), exactly
|
|
// retail's add_particle_shadow_to_cell (0x00514a70) membership. The
|
|
// former post-replay per-cell pass double-submitted the root flood's
|
|
// emitters and is deleted.
|
|
}
|
|
|
|
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
|
/// <see cref="DrawLandscapeThroughOutsideView"/> + <see cref="DrawBuildingLookIns"/>
|
|
/// split. The walk now draws every mesh and every attached emitter at its
|
|
/// own turn. This tail therefore carries only the two landscape services
|
|
/// that are not scene projections: ownerless outdoor-cell emitters and
|
|
/// weather. It deliberately submits no packed entity route.</summary>
|
|
private void DrawLandscapeDynamicsPhase(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
ClipFrameAssembly clipAssembly,
|
|
Walk.WalkFrameDriver walkDriver)
|
|
{
|
|
if (clipAssembly.OutsideViewSlices.Length != 0)
|
|
{
|
|
// 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);
|
|
}
|
|
|
|
// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
|
// (SetTerrainClip + ClearClipRouting + DrawLandscapeSliceLate, one
|
|
// call per active landscape view) is DELETED — retail draws the
|
|
// weather mesh and its rain particles ONCE, unclipped, after
|
|
// LScape::draw's whole landblock loop (GameSky::Draw(sky,1)
|
|
// @0x00506ff0), never once per doorway aperture. DrawWeatherOnce
|
|
// is the ONE call that remains — it now also submits the rain
|
|
// particles (moved from the deleted loop's per-slice, per-doorway
|
|
// ParticleRenderPass.SkyPostScene draw) as ONE unclipped
|
|
// submission, matching retail's one alpha-list insertion per
|
|
// emitter. The OC transcript print no longer lives here — it moved
|
|
// to WalkFrameDriver.OnWeatherTurn, fired by RetailFrameWalk.
|
|
// DrawLandscape at Collect time (S3 chunk 4, O3).
|
|
//
|
|
// S3 chunk 4 fix round 1 (K2): gated on walkDriver.WeatherTurnFired
|
|
// — the SAME predicate that gated the Collect-time "OC" transcript
|
|
// print (WalkFrameDriver.OnWeatherTurn, fired only when a Landscape
|
|
// turn actually ran with ctx.WeatherGateOpen open) — instead of
|
|
// independently re-deriving "clipAssembly.OutsideViewSlices.Length
|
|
// > 0", which could diverge from the print's gate on an interior
|
|
// root whose landscape turn ran but whose reassembled outside-view
|
|
// slices ended up empty (the transcript would then report a weather
|
|
// turn this call never actually drew). Gate closed -> neither the
|
|
// print nor this draw fires; gate open -> both do.
|
|
if (walkDriver.WeatherTurnFired)
|
|
passes.DrawWeatherOnce(ctx);
|
|
}
|
|
|
|
/// <summary>Campaign FW4 slice 2 — the walk-flood seal draw. Retail's
|
|
/// <c>PView::DrawCells</c> stamps every exit portal of THE FLOOD'S OWN
|
|
/// cells (pc:432785-432786, reverse cell_draw_list far→near) — one
|
|
/// visibility structure decides the flood, the seals, and the terrain
|
|
/// views alike. The old apparatus's flood misses exit portals at the
|
|
/// #456 cathedral seam band (its never-drawn panel family), leaving
|
|
/// aperture depth unsealed after the interior clear; the end-of-frame
|
|
/// alpha drain (cell-owned emitters — retail's own timing) then
|
|
/// z-passes across the whole opening (the falls shine-through). Each
|
|
/// portal is stamped once per exact walk-owned view captured for that
|
|
/// flood cell, matching retail's <c>CEnvCell::setup_view</c> loop. The
|
|
/// legacy visibility assembly has no production role here.</summary>
|
|
private int DrawWalkExitPortalMasks(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
Walk.WalkFrameDriver driver)
|
|
{
|
|
int submitted = 0;
|
|
List<uint> floodCells = driver.InteriorFloodCells;
|
|
for (int i = floodCells.Count - 1; i >= 0; i--)
|
|
{
|
|
uint cellId = floodCells[i];
|
|
int sliceCount = driver.InteriorFloodViewSliceCountAt(i);
|
|
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
|
|
{
|
|
submitted += passes.DrawExitPortalMask(
|
|
ctx,
|
|
cellId,
|
|
driver.InteriorFloodViewClipPlanesAt(i, sliceIndex));
|
|
}
|
|
}
|
|
return submitted;
|
|
}
|
|
|
|
private static RenderFrameDiagnosticCounts WalkDiagnosticCounts(
|
|
RenderProjectionCounts source)
|
|
{
|
|
int dynamics = checked(
|
|
source.LiveDynamicRoot
|
|
+ source.ActiveAnimatedStatic
|
|
+ source.EquippedChild);
|
|
return new RenderFrameDiagnosticCounts(
|
|
source.OutdoorStatic,
|
|
source.IndoorCellStatic,
|
|
dynamics,
|
|
TransformCount: source.Total,
|
|
OpaqueClassificationCount: 0,
|
|
AlphaClassificationCount: 0,
|
|
LightSetCount: 0,
|
|
SelectionPartCount: 0,
|
|
RouteCandidateCount: source.Total,
|
|
EntityCandidateCount: source.Total,
|
|
MeshPartCount: 0);
|
|
}
|
|
|
|
}
|
|
|
|
public interface IRetailPViewCellSource
|
|
{
|
|
LoadedCell? Find(uint cellId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Typed execution seam for the GL passes ordered by
|
|
/// <see cref="RetailPViewRenderer"/>. Implementations execute the requested
|
|
/// pass only; visibility construction and draw ordering remain renderer-owned.
|
|
/// All frame inputs and results are borrowed for the duration of the call.
|
|
/// </summary>
|
|
public sealed class RetailPViewFrameInput
|
|
{
|
|
public LoadedCell RootCell { get; private set; } = null!;
|
|
|
|
/// <summary>R-A2: nearby building cells (BuildingId-tagged) flooded per-building when the root is the
|
|
/// outdoor node. Null for interior roots. Grouped by BuildingId inside <see cref="DrawInside"/>.</summary>
|
|
public IReadOnlyList<LoadedCell>? NearbyBuildingCells { get; private set; }
|
|
|
|
public Vector3 ViewerEyePos { get; private set; }
|
|
public Matrix4x4 ViewProjection { get; private set; }
|
|
public IRetailPViewCellSource Cells { get; private set; } = null!;
|
|
public ICamera Camera { get; private set; } = null!;
|
|
public Vector3 CameraWorldPosition { get; private set; }
|
|
public FrustumPlanes? Frustum { get; private set; }
|
|
public uint? PlayerLandblockId { get; private set; }
|
|
public HashSet<uint>? AnimatedEntityIds { get; private set; }
|
|
public int RenderCenterLbX { get; private set; }
|
|
public int RenderCenterLbY { get; private set; }
|
|
public int RenderRadius { get; private set; }
|
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
|
{ get; private set; } = Array.Empty<(uint, Vector3, Vector3,
|
|
IReadOnlyList<WorldEntity>,
|
|
IReadOnlyDictionary<uint, WorldEntity>?)>();
|
|
|
|
// Pass-presentation and diagnostic values consumed synchronously by the
|
|
// typed executor. This input is data-only and is never retained.
|
|
public bool RenderSky { get; private set; }
|
|
public bool RenderWeather { get; private set; }
|
|
public float DayFraction { get; private set; }
|
|
public DayGroupData? ActiveDayGroup { get; private set; }
|
|
public SkyKeyframe SkyKeyframe { get; private set; }
|
|
public bool EnvironOverrideActive { get; private set; }
|
|
public uint ViewerCellId { get; private set; }
|
|
public uint PlayerCellId { get; private set; }
|
|
public Vector3 PlayerViewPosition { get; private set; }
|
|
public Matrix4x4 CameraView { get; private set; }
|
|
public CameraCellResolution CameraCellResolution { get; private set; }
|
|
|
|
internal RetailPViewFrameInput Reset(
|
|
LoadedCell rootCell,
|
|
IReadOnlyList<LoadedCell>? nearbyBuildingCells,
|
|
Vector3 viewerEyePos,
|
|
Matrix4x4 viewProjection,
|
|
IRetailPViewCellSource cells,
|
|
ICamera camera,
|
|
Vector3 cameraWorldPosition,
|
|
FrustumPlanes? frustum,
|
|
uint? playerLandblockId,
|
|
HashSet<uint>? animatedEntityIds,
|
|
int renderCenterLbX,
|
|
int renderCenterLbY,
|
|
int renderRadius,
|
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
|
bool renderSky,
|
|
bool renderWeather,
|
|
float dayFraction,
|
|
DayGroupData? activeDayGroup,
|
|
SkyKeyframe skyKeyframe,
|
|
bool environOverrideActive,
|
|
uint viewerCellId,
|
|
uint playerCellId,
|
|
Vector3 playerViewPosition,
|
|
Matrix4x4 cameraView,
|
|
CameraCellResolution cameraCellResolution)
|
|
{
|
|
RootCell = rootCell;
|
|
NearbyBuildingCells = nearbyBuildingCells;
|
|
ViewerEyePos = viewerEyePos;
|
|
ViewProjection = viewProjection;
|
|
Cells = cells;
|
|
Camera = camera;
|
|
CameraWorldPosition = cameraWorldPosition;
|
|
Frustum = frustum;
|
|
PlayerLandblockId = playerLandblockId;
|
|
AnimatedEntityIds = animatedEntityIds;
|
|
RenderCenterLbX = renderCenterLbX;
|
|
RenderCenterLbY = renderCenterLbY;
|
|
RenderRadius = renderRadius;
|
|
LandblockEntries = landblockEntries;
|
|
RenderSky = renderSky;
|
|
RenderWeather = renderWeather;
|
|
DayFraction = dayFraction;
|
|
ActiveDayGroup = activeDayGroup;
|
|
SkyKeyframe = skyKeyframe;
|
|
EnvironOverrideActive = environOverrideActive;
|
|
ViewerCellId = viewerCellId;
|
|
PlayerCellId = playerCellId;
|
|
PlayerViewPosition = playerViewPosition;
|
|
CameraView = cameraView;
|
|
CameraCellResolution = cameraCellResolution;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Borrowed renderer scratch valid only until the next
|
|
/// <see cref="RetailPViewRenderer.DrawInside"/> call. Collections and nested
|
|
/// frame objects are deliberately reused to keep the render loop allocation
|
|
/// free; consumers must copy any state they need to retain asynchronously.
|
|
/// </summary>
|
|
public sealed class RetailPViewFrameResult
|
|
{
|
|
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
|
public HashSet<uint> DrawableCells { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The production retail walk's exact visited-cell set. This is the one
|
|
/// visibility answer consumed by EnvCell preparation, particles, lights,
|
|
/// and directional-shadow filtering.
|
|
/// </summary>
|
|
public HashSet<uint> VisibleCells { get; private set; } = null!;
|
|
|
|
internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; }
|
|
internal RenderProjectionCounts SourceCounts { get; private set; }
|
|
internal InteriorEntityPartition.Result? DiagnosticPartition
|
|
{ get; private set; }
|
|
|
|
internal RetailPViewFrameResult Reset(
|
|
ClipFrameAssembly clipAssembly,
|
|
HashSet<uint> drawableCells,
|
|
HashSet<uint> visibleCells,
|
|
RenderFrameDiagnosticCounts diagnosticCounts,
|
|
RenderProjectionCounts sourceCounts,
|
|
InteriorEntityPartition.Result? diagnosticPartition)
|
|
{
|
|
ClipAssembly = clipAssembly;
|
|
DrawableCells = drawableCells;
|
|
VisibleCells = visibleCells;
|
|
DiagnosticCounts = diagnosticCounts;
|
|
SourceCounts = sourceCounts;
|
|
DiagnosticPartition = diagnosticPartition;
|
|
return this;
|
|
}
|
|
|
|
}
|