acdream/src/AcDream.App/Rendering/RetailPViewRenderer.cs
Erik 89c4494d05 fix(render): S3 review round 1 — punch fans clipped by their own view, seal pin through the producer, dead per-cell clip machinery deleted, EC/OC transcript exact
F1 (BLOCKING, punch-fan view alignment). ClipFrameAssembler.
ReassembleOutsideViewFromWalk now keeps exactly ONE outside-view slice per
walk view, index-aligned with RetailFrameWalk.DrawBuilding's own
ActiveViewIndex (retail building_view = portal_view_num @0x0059f3bf): a
collapsed view (ClipPlaneSet.IsNothingVisible — retail polyClipFinish under
a degenerate view leaves <3 vertices, 0x59BDBC-0x59BDBF) now appends its
own ClipViewSlice.NothingVisible=true slot instead of being skipped, which
used to shift every later view's slice down by one. DrawWalkPunchFan draws
NOTHING for a NothingVisible slice and throws ArgumentOutOfRangeException
when activeViewIndex is out of range (fail-loud; never draws unclipped).
New tests: WalkOutsideViewReassemblyTests.
FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned and
PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange.
MUTATION M1 (revert the append-nothing-on-skip fix, restoring the old
skip): both new tests fail with
  Assert.Equal() Failure: Values differ
  Expected: 2
  Actual:   1
MUTATION M2 (restore the old ": default" unclipped fallback in
DrawWalkPunchFan): the punch-leaf test fails with
  Assert.Equal() Failure: Values differ
  Expected: 0
  Actual:   1
(both restored; verified clean afterward).

F2 (major, exit-seal pin through the real producer + both overflow cases).
The exit-seal CPU/GPU equivalence pin moved from ClipFrameLayoutTests (which
drove ClipFrame.AppendSlot(ClipPlaneSet) via the zero-caller
ClipFrameAssembler.Assemble) to the new
tests/.../Walk/WalkFrameDriverClipSealTests.cs, which drives
WalkFrameDriver.BeginFrame + the real IWalkEventSink.OnInteriorFloodDrawTurn
hook — the SAME path RetailPViewPassExecutor.DrawExitPortalMask resolves
through via WalkFrameDriver.InteriorFloodViewClipPlanesAt. The new pin
compares WalkFrameDriver's captured planes EXACTLY (not just geometrically)
against ClipPlaneSet.From's independent CCW-normalized-perpendicular
computation, since AppendClipSlot's per-edge formula is bit-identical to
ClipPlaneSet's own. Two overflow cases added: a 9-vertex portal view exit-seals
to exactly the 4 conservative AABB planes containing every source vertex
(over-include, never under-include — AppendClipSlot's fallback for a view too
complex for the 8-plane budget); a 9-vertex OUTSIDE view (the punch-fan
sibling, WalkOutsideViewReassemblyTests.
NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible)
produces a zero-plane, NOT-NothingVisible slice — draws fully unclipped,
distinct from F1's "draw nothing" state.
MUTATION M1 (flip the winding selection, `ccw = area2 < 0f`):
  Assert.Equal() Failure: Values are not within 4 decimal places
  Expected: 0.9191 (rounded from 0.919144988)
  Actual:   0.8944 (rounded from 0.89442724)
MUTATION M2 (drop the normalize from the per-edge plane formula):
  Assert.Equal() Failure: Values are not within 4 decimal places
  Expected: 0.9191 (rounded from 0.919144988)
  Actual:   0.7 (rounded from 0.700000048)
MUTATION M3 (delete the >8-plane AABB overflow branch — the per-edge loop
then indexes a fixed 8-plane stackalloc with a 9-edge polygon):
  System.IndexOutOfRangeException : Index was outside the bounds of the array.
    at WalkFrameDriver.AppendClipSlot(...) line 1915
    at WalkFrameDriver.CaptureViews(...) -> CaptureCellViews -> CaptureCellViewRoute
    -> EmitFloodTurns -> IWalkEventSink.OnInteriorFloodDrawTurn
(all three restored; verified clean afterward).

F3 (minor/major). RetailPViewPassExecutor.cs's false bracket comment
(neither KEEP clip — exit seals nor punch fans — ever called Enable/
DisableClipDistances; the six DisableClipDistances() call sites all sat
around the UNCLIPPED particle/weather draws, whose bodies are no-ops on the
only backend) is rewritten to the truth; the six no-op calls and the
private DisableClipDistances() wrapper are deleted.
IWorldPassSurface.EnableClipDistances/DisableClipDistances stay on the
INTERFACE — WorldScenePassExecutor (the separate flat-world path) still
calls them.

F4a (EC transcript exactness). WalkFrameDriver.EmitFloodTurns now prints one
"EC" line per LIVE VIEW of the flood cell (retail's setup_view/DrawEnvCell
loop, PView::DrawCells @0x005a4ab1-0x005a4acc, fires once per live portal_view
slice, not once per cell) via the captured route's own SliceCount — read
directly off _lookInTurns[viewRouteIndex] rather than through
InteriorFloodViewSliceCountAt's flood-index indirection, because this same
loop also serves a building's LOOK-IN flood (WalkDrawStage.LookInStatic),
whose cells never populate InteriorFloodCells (the interior ROOT flood only)
— indexing through that accessor threw ArgumentOutOfRangeException on a real
look-in turn during verification, fixed before landing. The actual CellShell
DRAW event is unchanged (the frame stamp still dedupes it to one submission).

F4b (transcript exactness, trailing weather OC).
WalkTraceConformanceTests.Recorder now implements IWalkEventSink.
OnWeatherTurn (the interface's silent no-op default previously left the
replay side of Signature8 blind to the weather turn's own "OC" line), and
WalkTraceReplayContext.Signature8(WalkOracleFrame) no longer excludes the
trailing per-frame weather ObjectCellTurn — every ObjectCellTurn now reads
literally, on both sides. Implementing OnWeatherTurn alone left
Still_fixture_first_frame_reproduces_exactly(terrace-edge.walk) diverging
ONLY on the newly-un-excluded trailing OC (EXPECTED ...OC:f4180104|
OC:f418000b vs ACTUAL ...OC:f4180104 — a length-12 tail, nothing else
differs across a 16.6k-char signature) because
WalkTraceReplayContext.WeatherGateOpen had always defaulted to false (the
harness never needed to fire the walk's weather hook before). Per the
review round's own instruction not to weaken the pin, the harness itself is
fixed instead: WalkTraceReplayContext now implements ViewerCellId (= the
harness's own WalkFrame cameraCellId argument, matching production's
RetailPViewFrameInput.ViewerCellId invariant) and WeatherGateOpen
(retail's own SmartBox::is_player_outside gate, (cellId & 0xFFFF) < 0x100,
with no App-level render-toggle concept to AND against). MUTATION (delete
Recorder.OnWeatherTurn): Still_fixture_first_frame_reproduces_exactly
(terrace-edge.walk) fails with EXPECTED ending "...OC:f4180104|OC:f418000b"
vs ACTUAL ending "...OC:f4180104" (the trailing OC missing) — restored;
verified clean afterward.
Fixture-row status (InstalledDat lane, WalkTraceConformanceTests, 14 rows):
13 pass, 1 fails — Oh_doorway_still_first_frame_diff, the PRE-EXISTING
[Trait("Status","KnownFailure")] #458 row (a documented block-plane
precision boundary at token index 165, LC/SC content, upstream of any
EC/OC — unaffected by and unrelated to this round's OC change). Every OTHER
row, including every eight-kind (Signature8) row that now compares the
weather OC literally, passes exactly.

F5 (major, dead per-cell clip machinery). WbDrawDispatcher.SetClipRouting
(the only writer of the per-instance routing-active flag) had ZERO
production callers; EnvCellRenderer.SetClipRouting was called only with
null (WorldScenePassExecutor's BeginFrame/AbortFrame) — no path could ever
arm the per-cell clip-region table, on the walk path OR the flat path (the
S3 landing-hygiene AD-17 correction's "live for the flat path" clause is
itself corrected here: dormant in every path, deleted). Deleted:
WbDrawDispatcher's SetClipRouting/ClearClipRouting and their four backing
fields; EnvCellRenderer's SetClipRouting and its backing field (its
RenderModernMDIInternal write now unconditionally clears instanceClipSlot);
the ProbeClipRouteEnabled-gated per-frame [clip-route-disp] dispatch probe
and its three backing fields (the OTHER two ACDREAM_PROBE_CLIPROUTE
producers, [clip-route] and [clip-route-scis] in WorldRenderDiagnostics.cs,
are untouched — a different, still-live mechanism); ResolveEntitySlot,
IsIndoorCellId and the ClipSlotCull sentinel; the ACDREAM_CLIP_DEBUG probe
(RenderingDiagnostics.ClipDebugNoShellTrim) and its docs/launch-options.md
row, since the "clip shells" branch it toggled between is gone.
ResolveSlotForFrame is simplified to a parameterless
`(uint Slot, bool Culled) ResolveSlotForFrame() => (0u, false)` per the
review's own framing ("it becomes slot 0, never culled") rather than
deleted outright, since its callers (WbDrawDispatcher.cs and
WbDrawDispatcher.WalkClassify.cs) still want the same two-value shape.
tests/.../Wb/WbDrawDispatcherClipSlotTests.cs (12 tests, all exercising the
deleted routing arm via ResolveEntitySlot/the active ResolveSlotForFrame
branch) is replaced with one pin on the new parameterless behavior.
Shaders: mesh_modern.vert, mesh_atmospheric.vert, mesh_detail.vert,
particle.vert and particle_mesh.vert each lose their CellClip struct +
binding=2 ClipRegionBuf fetch, gl_ClipDistance write loop, and gl_PerVertex
redeclaration (portal_depth.vert's own, separate gl_PerVertex/gl_ClipDistance
pair — the KEEP mechanism, its planes handed through the TerrainClip UBO at
binding=2 in the UBO namespace — is untouched; verified it never read the
deleted SSBO). Recompiled via tools/compile-shaders.ps1 (glslc backend
detected; managed shaderc path ran); 24/24 pairs compiled. Re-pinned in
VulkanShaderManifestTests.RetailOracleSpirvSha256 (the only three of the
five edited shaders that carry a byte-exact retail-oracle pin):
  mesh_modern.vert.spv    = f9ed4ee7140ccd136130559dbea68545f733f6022f52b085b6df4bcb787223c6
  particle.vert.spv       = 95ce6ecf834930a92da5c5fe9aef513b38b5ba104704b98c1606af71fe17eaf3
  particle_mesh.vert.spv  = 043482b97c2ed036511692f89c75a0a6c298aba48cb519e5e3aff7fe7ba6371b
(mesh_atmospheric.vert.spv / mesh_detail.vert.spv changed too but carry no
retail-oracle pin, so no re-pin was needed for them.)
tests/.../ParticleBindlessInstanceTests.cs's
Assert.Contains("clipRegions[aClipSlot]", vertex) — a real breakage the
grep sweep caught — is deleted with a note; the surrounding aClipSlot
attribute-declaration assertion stays.
Vertex-layout residue kept THIS round, fed 0 by the CPU, unread by any
shader (S5's instance-buffer-layout revisit): instanceClipSlot[] (binding=3
SSBO) in mesh_modern.vert/mesh_atmospheric.vert/mesh_detail.vert; aClipSlot
(a per-vertex attribute) in particle.vert/particle_mesh.vert.
Grep sweep — "SetClipRouting|_clipRoutingActive|ClearClipRouting|
BeginScissor|NdcScissorRect|SetTerrainClip|BindTerrainClip|TerrainBytes|
ScissorNdcAabb|UseScissorFallback" over src/ and tests/: EMPTY (also swept
and rewrote several PRE-EXISTING prose mentions of BeginScissor/
ClearClipRouting in RetailPViewPassExecutor.cs, RetailPViewRenderer.cs,
WorldPassSurface.cs and RetailPViewPassExecutorTests.cs that predate this
commit's own work, purely to satisfy the literal sweep). "clipRegions|
instanceClipSlot|aClipSlot": every remaining match is the vertex-layout
residue named above, or a comment/assertion describing that same residue.

Deviations from a fully literal reading of F5 (recorded here since none of
them cross the mandatory automated gates, all of which pass):
- The C#-side ClipRegions SSBO publish/bind pipeline
  (RhiWorldPassSurface.PrepareClipFrame's publish, WorldFrameSections.
  ClipRegions, and ClipFrame's internal std430 byte-packing —
  RegionBytes/RegionBytesForTest/CellClipStrideBytes) is NOT deleted this
  round; it is left as documented dead residue (verified zero shader
  readers) rather than rewritten. The mandatory grep sweep targets the
  shader-source identifier `clipRegions` (lowercase), which is fully clean;
  the C# publish machinery sits behind ~8 files (ClipFrame.cs,
  WorldPassSurface.cs, WbDrawDispatcher.Rhi.cs, WorldPassScope.cs,
  GpuBindingModel.cs, EnvCellRenderer.Rhi.cs, ParticleRenderer.Rhi.cs,
  WbDrawDispatcher.OrderedStream.cs, plus the ClipFrameLayoutTests std430
  pins) and CellClipPlanesOffset/MaxPlanes are the SAME constants
  portal_depth.vert's KEEP mechanism uses for its own, unrelated UBO layout
  — a full rewrite carried materially higher regression risk than this
  round's mandatory scope justified. ClipFrame.cs and WorldPassSurface.cs
  now both name this residue explicitly in their doc comments for a future
  round to finish.
- SetClipRegionSsbo/_sharedClipRegionSsbo in WbDrawDispatcher.cs (a
  pre-existing, already fully dead GL-era write-only field/method with zero
  callers, unrelated to per-instance clip-slot routing) is left untouched —
  outside F5's named scope and outside the mandatory grep sweep.
- WalkTraceReplayContext.ViewerCellId/WeatherGateOpen (F4b, detailed above)
  is an addition beyond the literal §12 text, made to keep the InstalledDat
  lane at exactly the four established known failures without weakening
  the newly-un-excluded OC pin.

Gates: dotnet build tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c
Release and dotnet build src/AcDream.App/AcDream.App.csproj -c Release both
0 warnings/0 errors; dotnet build AcDream.slnx -c Release 0 warnings/0
errors; hermetic lane 6815/6815 passed; InstalledDat lane 243 passed / 4
failed / 1 skipped (exactly the four known failures: two #383 layout tests,
TowerAscentReplayTests, Oh_doorway_still_first_frame_diff #458);
VulkanShaderDescriptorContractTests/VulkanShaderManifestTests/
RenderPackSpirvValidatorTests/SkyVertexLayoutTests 34/34 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 20:15:29 +02:00

878 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). S3 landing hygiene
// (H2) deleted the GL-era arena-reservation count this call used to
// compute and pass — PrepareClipFrame() takes nothing now (see
// IWorldPassSurface.PrepareClipFrame's doc).
passes.PrepareClipFrame();
// 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&gt;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&gt;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
// (the walk's own screen-space terrain-clip writer + its per-frame
// clip-routing reset call + 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) call site
// @0x00506396 — the callee itself is @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;
}
}