acdream/src/AcDream.App/Rendering/RetailPViewRenderer.cs
Erik 9ccb61a8ec fix(render): S4 chunk 2 round 1 - particle throw, detail-off immediate draws, EnvCell FIFO, gate honesty
Fix round 1 of the two the plan allows for S4-c2 (048d5b12f), addressing
every item M1-M8/A1-A8 from the campaign branch's packet section 11.

M1 (BLOCKING): ParticleRenderer.DeferToRetailAlphaQueue threw whenever a
mesh-particle batch's TranslucencyKind.Opaque classification (mask 0x00)
reached a non-Append DrawMesh row - a real, reachable outcome since
TryAppendMeshDraws applies no IsOpaque-style pre-filter unlike
WbDrawDispatcher's path. Deleted both throws (ParticleRenderer.cs,
WbDrawDispatcher.cs SubmitToAlphaQueue), extracted the router-input
derivation into internal static ParticleRenderer.RouteParticleSubmission
(directly testable), and implemented rows 1/5's immediate draw by reusing
the existing per-instance billboard/mesh draw code (new
DrawImmediateParticleSubmissionRhi, no new renderer, no sort per A8).
materialHasAlpha (row 4) is derived from CMaterial::CheckAlphaValues
@0x005396a0 (Ghidra-verified 2026-09-04): has_alpha=0 only when
Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else 1 - and
CMaterial::SetTranslucencySimple @0x005396f0 (reached from
CPhysicsPart::SetTranslucency <- Particle::Update @0x0051c290's per-frame
alpha-fade) sets all four channels to (1f - translucency) uniformly, so
has_alpha == "this particle's current alpha isn't 1.0" - already carried in
ColorArgb's top byte (Lerp(StartAlpha,EndAlpha,tLife), ParticleSystem.cs
1039-1041/1453).

M3: SubmitToAlphaQueue now takes isBuildingShell + viewProjection and feeds
Row 1's detailSurfaceActive gate for translucent building-shell instances
(retail DrawBuilding @0x0059f2a0 installs building detail before its own
shell subsets reach DrawMesh). Row 1 (detail on) draws the instance
immediately at its own walk-stream AlphaSubmitMark via new
DrawImmediateAlphaInstance/DrawImmediateAlphaInstanceRhi (reusing
WriteDeferredAlphaEntrySlot + the drain's pipeline/detail code for slot 0);
never touches _deferredAlpha/the queue for pure-Immediate submissions (fixes
a latent leak: previously reserving a _deferredAlpha slot for every
candidate meant a frame with ALL-immediate content never registered a
source, so ResetAlphaSubmissions never ran). M3c: DrawPreparedAlphaBatchRhi's
own per-command hasDetail/DrawBuildingDetailRangeRhi branch is deleted -
detail-eligible content no longer reaches the drain (spec: delayed replay
is always detailEnabled=0). New AD-120 register row for the "in place -> at
the entity's own stream mark" granularity adaptation this introduces.

M6: RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell routes an
EnvCell's transparent shell through the SAME RetailAlphaMeshRouter table
(mask 0x02, detailSurfaceActive = EnvCellRenderer.TransparentDetailEnabled)
instead of a hand-coded branch. Detail on: unchanged immediate draw. Detail
off: appends ONE token per CELL via new EnvCellAlphaDrawSource (internal,
delegate-based so it's unit-testable without a GPU harness), replayed
through EnvCellRenderer.RenderTransparentOrdered at the next flush -
RenderTransparentOrdered already replayed a cell's batch on demand, so no
new deferred-replay abstraction was needed (the original landing's claim
that one was required is corrected in the packet).

M4: RetailAlphaEntry.IsFirstForList deleted. Retail's per-DrawMesh-
invocation "first for this list" flag is trivially true for every subset
retail ever appends (DrawMesh @0x0059d4a0 sets both flags true at entry,
0059d4cc/0059d4d0, clearing each independently after ITS OWN first append
within that call, 0059d5ef) - the captures confirm it (new=1 on 9,685/10,556
AM lines, impossible under "first since last drain"). acdream's deleted
isFirstForList = target.Count == 0 computed the wrong quantifier. One
acdream append already IS one DrawMesh call, so the field carried no
information and its own pin was vacuous (feedback_every_new_pin_must_be_
shown_to_fail) - deleted rather than kept as an always-true no-op.

M5: split all six AlphaFlushTranscript_* KnownFailure Facts into live
AlphaFlushSites_* (site+threshold sequence only, content-independent,
unconditional pass) and AlphaFlushCounts_* (KnownFailure, full tuple incl.
drained counts) sharing one RunAlphaFlushTranscriptReplay helper. Rewrote
the KnownFailure reason honestly: THREE independent causes, not "harness
content only" - (1) zero live mesh content, (2) acdream appends one entry
per INSTANCE vs retail's one per SUBSET per DrawMesh call, (3) acdream's
CLIP list is structurally empty for ordinary content (M2). Added ONE live
valve pin (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAtThe
ExactBoundary, a Theory over 2250/2249) that goes through WalkFrameDriver.
Replay's own SortCellExit arm, not RetailAlphaQueue.Flush called directly.

M2/A2/M8c (register): ~~AP-34~~ restored as struck-through RETIRED (it had
been deleted outright, not struck, breaking the register's own convention)
citing residuals AP-238/AP-239/AP-240. The chunk-2 commit's "AP-236"/
"AP-237" collided with an id ALREADY filed-and-retired on main by an
unrelated #132 fix (docs/ISSUES.md:19976) - ids are never reused, so both
are renumbered: AP-236->AP-238 (EnvCell; rewritten per M6 - granularity-only
residual now, "never through the shared queue" is retired), AP-237->AP-239
(mask-reconstruction gap; the dangling "AP-236b" citation in
RetailAlphaMeshRouter.cs never matched either filed id and is corrected to
AP-239). New AP-240 filed for M2: WbDrawDispatcher.IsOpaque filters
ClipMap out upstream, so ordinary clip-mapped content never reaches
retail's CLIP FIFO (draws immediately on the pre-existing
OpaqueAlphaToCoverage pipeline instead) - no pipeline change this round,
per the contract. AP section header recounted honestly: 159 active (was
stale at 161); AD section 92 (was 91, +AD-120).

M7: AP-239's only cited example (cloud GfxObj 0x01004C35, surface
0x08000023) is DISPROVEN - decoded Type=0x10114 has alpha-family bits
that win in BOTH retail's real priority and acdream's reconstruction, so it
never actually diverges. A 2026-09-04 scan of all 6,152 Portal-dat Surfaces
found 27 real Translucent+Base1ClipMap-no-alpha-family instances
(Type=0x14); cross-referencing GfxObjs found surface 0x08000015 on GfxObj
0x010001EC - cited in place of the disproven cloud example.

M8: (a) RenderDeviceD3D::DrawBlock is @0x005a17c0 per symbols.json, not
@0x005a18d0 (that is its per-land-cell loop head) - fixed across
RetailAlphaQueue.cs, RetailPViewPassExecutor.cs, WalkEvents.cs,
WalkFrameDriver.cs (x3), RetailFrameWalk.cs. (b) RetailAlphaQueue.cs's four
FlushAlphaList addresses were labelled "return site" but are the CALL
instruction's own address (x86 CALL rel32 is 5 bytes) - relabelled "call at
X (returns to X+5)". (d) worldbuilder-inventory.md:420's AP-34 citation
updated.

A1: RetailPViewRenderer.cs's outdoor-root passes.FlushLandscapeAlpha() call
(labelled LandscapeFlush) is deleted - retail's real LandscapeFlush
(PView::DrawCells @0x005a4840's own FlushAlphaList(0f) @0x005a4872) sits
strictly inside if (outside_view.view_count > 0) on the INTERIOR PView; an
outdoor root never reaches it (terrace-edge capture: zero 005a4877 FL
lines). Verified no opaque draw intervenes between this deletion point and
RetailAlphaQueue.EndFrame()'s own RenderNormalMode pass-end flush
(WorldSceneRenderer.cs: DrawPostWorldParticles's PView branch is an
explicit no-op; only diagnostic wireframes follow) - #132's "drain after
the finished opaque world" invariant is kept by that flush alone.

A4: RetailAlphaQueue's ApplyScratchRetention call sites passed the ENTRY
count as the SOURCE-count argument too (observedClip + observedAlpha
twice), so _sources/_sourceDrawOffsets never shrank toward the real source
count. Fixed to capture _sources.Count before Clear().

A5/A6/A7: WalkStaticStreamPopulatorTests.cs's stale "same viewer distance"
banner corrected; two off-by-one entry counts in the packet's section 10
sequences corrected (foundry-deep x10->x11, holtburg-doorway-still x5->x6);
WalkAlphaDepthTrace.cs's "new=" comment explains why IsNew is retained but
never compared (M4's finding makes a per-subset comparison meaningless at
acdream's per-instance granularity); RetailAlphaMeshRouterTests.cs's
IndependentReferenceRoute (never actually independently-shaped) renamed to
RestatedBranchTableRoute with an honest doc comment, and the unverified
"flipped in turn and failed every time" claim deleted.

A3 was resolved as a side effect of M1's SubmitToAlphaQueue doc-comment
rewrite (the wrong "0x02/0x03, 0x04/0x05, 0x08/0x09" enumeration no longer
exists); RetailAlphaMeshRouter.cs's own MaskFromTranslucencyKind comment
updated in the same spirit (M1's particle-site Opaque-reachability finding).

Mutation checks (each applied to a scratch-restored copy, confirmed
failing, then byte-exact reverted - diffed against a /tmp backup after
restore):

- A1 (RetailPViewRendererTests.DrawInside_NeverCallsFlushLandscapeAlphaDirectly):
  restoring the deleted "if (ctx.RootCell.IsOutdoorNode)
  passes.FlushLandscapeAlpha();" line reintroduces a direct
  RetailPViewPassExecutor.FlushLandscapeAlpha call in DrawInside's compiled
  body. Actual failure: "Assert.DoesNotContain() Failure: Filter matched in
  collection ... CompiledCall { ... Target = Void FlushLandscapeAlpha() }".

- A4 (RetailAlphaQueueTests.RetainedSourceCapacity_ConvergesToTheReal
  SourceCountNotTheEntryCount): reverting both ApplyScratchRetention calls
  to (observedClip + observedAlpha, observedClip + observedAlpha) - with
  100 distinct sources in the spike frame (so _sources.Capacity grows to
  128, past its initial 4) followed by three low-demand frames of 10
  entries via ONE repeated source - makes sourceTarget compute from 10
  (Math.Max(4,10*2)=20) instead of 1 (Math.Max(4,1*2)=4). Actual failure:
  "Expected the retained source-array capacity to converge toward the real
  source count (1), but it stayed at 20". (A single-source-only spike does
  NOT discriminate this bug - Math.Min(sourceTarget, _sources.Capacity)
  clamps both formulas to the same unchanged initial 4 - hence the
  100-source setup.)

- M5 valve (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAt
  TheExactBoundary, 2250 case): flipping RetailAlphaQueue.Flush's no-op
  comparison from "<" to "<=" (S4-c1/c2's own boundary mutation) makes the
  2250-entries case a no-op instead of draining. Actual failure:
  "Assert.Equal() Failure: Values differ Expected: 0 Actual: 2250".

- M6 (EnvCellAlphaDrawSourceTests, both Facts): adding an immediate
  renderTransparentOrdered(new[]{cellId}) call inside
  EnvCellAlphaDrawSource.AddPendingCellId (simulating the M1/M3 "drew
  immediately instead of deferring" bug class) makes
  PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt fail with
  "Assert.Empty() Failure: Collection was not empty Collection: [[4660]]"
  and ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombined
  Drain fail with 'Expected: string[] ["cell:256", "particle:7",
  "cell:512"] Actual: List<string> ["cell:256", "cell:512", "cell:256",
  "particle:7", "cell:512"]'.

- M1 (ParticleRendererRouteTests.OpaqueClassifiedMeshBatch_WithMaterialAlpha
  _RoutesToAlphaAppend): hardcoding materialHasAlpha to false inside
  RouteParticleSubmission makes the Opaque-mask+has-alpha case route
  Immediate instead of Append. Actual failure: "Assert.Equal() Failure:
  Values differ Expected: Append Actual: Immediate".

Gates (real output lines):

- dotnet build AcDream.slnx -c Release -> "Build succeeded. 0 Warning(s)
  0 Error(s)".
- Hermetic lane (--filter "Lane!=InstalledDat&Lane!=PreparedPackage&
  Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&
  Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure") ->
  "Passed! - Failed: 0, Passed: 6864, Skipped: 0, Total: 6864".
- InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1,
  ACDREAM_DAT_DIR="C:/Users/erikn/Documents/Asheron's Call",
  --filter "Lane=InstalledDat") -> "Failed: 10, Passed: 255, Skipped: 1,
  Total: 266" - the exact 4 pre-existing failures
  (TowerAscent_StaircaseStaysConeVisible_EveryStep,
  LayoutImporterMediaBearingChildSweepTests, LayoutImporterInvisibleSweep
  Tests, Oh_doorway_still_first_frame_diff) plus the six
  AlphaFlushCounts_* KnownFailure Facts, nothing else; all six
  AlphaFlushSites_* Facts pass inside this same lane (the whole
  WalkTraceConformanceTests partial class carries Lane=InstalledDat, so
  they cannot run hermetically).
- Shader classes (VulkanShaderDescriptorContractTests,
  VulkanShaderManifestTests, RenderPackSpirvValidatorTests) -> "Passed! -
  Failed: 0, Passed: 32, Skipped: 0, Total: 32".
- RetailAlphaQueueTests, RetailAlphaMeshRouterTests, RetailFrameWalkTests,
  WalkFrameDriverTests, ParticleRenderer*Tests, EnvCellRenderer*Tests
  (combined filter) -> "Passed! - Failed: 0, Passed: 120, Skipped: 0,
  Total: 120".
- Register collision grep: AP-238/AP-239/AP-240/AD-120 each count exactly
  1; ~~AP-34~~ count exactly 1; live totals AP=159, AD=92.

Not done / deferred: none - all M1-M8/A1-A8 items landed; A8 was a
no-action note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 11:53:03 +02:00

897 lines
43 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();
}
// S4-c2 fix round 1 (A1): the call that used to sit HERE
// (`passes.FlushLandscapeAlpha()`, an outdoor-root-only drain
// labelled LandscapeFlush) is DELETED — it was mislabelled.
// Retail's LandscapeFlush (`PView::DrawCells` @0x005a4840's own
// `FlushAlphaList(0f)` @0x005a4872) sits strictly INSIDE
// `if (outside_view.view_count > 0)` on the INTERIOR PView
// (WalkEvents.cs's own `OnInteriorFloodDrawTurn` doc comment);
// an outdoor root never reaches that branch at all — confirmed
// by the terrace-edge capture (zero `005a4877` FL lines) and by
// `RetailAlphaQueue`'s own flush-site enum, whose
// `LandscapeFlush` member cites that exact address. Retail's
// real outdoor-root drains are `DrawBlock`'s per-cell 0.75 valve
// (`WalkFrameEventKind.SortCellExit`, already wired) and
// `SmartBox::RenderNormalMode`'s own `FlushAlphaList(0f)`
// @0x00453b8b at the pass end — which acdream already performs
// unconditionally in `RetailAlphaQueue.EndFrame()` (called from
// `WorldSceneRenderer.CompleteWorldFrame()`). Between this
// method's `return result;` below and that `EndFrame()` call,
// `WorldSceneRenderer.Render` performs zero further opaque-world
// draws for a PView-rooted frame — `DrawPostWorldParticles`'s
// own PView branch is an explicit no-op ("already drew inside
// LScape::draw... replaying here would be too late"), and the
// diagnostics that follow only emit collision wireframes (a
// debug-only probe, off by default) — so #132's "drain after
// the finished opaque world" invariant is kept by the
// RenderNormalMode flush alone, and `DrawUnattachedSceneParticles`
// below joins the SAME final drain retail's own outdoor root
// would produce (never an extra LandscapeFlush call retail
// never makes there).
// 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;
}
}