755 lines
36 KiB
C#
755 lines
36 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
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 PortalVisibilityFrame _mainPortalFrameScratch = new();
|
|
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
|
private readonly RetailPViewFrameResult _frameResultScratch = new();
|
|
|
|
private static readonly ClipViewSlice NoClipSlice =
|
|
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
|
|
private static readonly ClipViewSlice[] NoClipSlices = { NoClipSlice };
|
|
private static readonly IReadOnlySet<uint> NoParticleOwners =
|
|
new HashSet<uint>();
|
|
|
|
private readonly HashSet<uint> _cellParticleOwnerScratch = 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();
|
|
|
|
// 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;
|
|
|
|
// ACDREAM_PROBE_WALK_ROOT (FW3 visual-gate apparatus, throwaway): the
|
|
// previous frame's root kind + a post-flip frame countdown so each
|
|
// interior/outdoor transition dumps 8 frames of rooting facts.
|
|
private bool? _probeWalkRootPrevOutdoor;
|
|
private int _probeWalkRootFramesLeft;
|
|
private ulong _probeWalkRootFrame;
|
|
|
|
// 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)
|
|
{
|
|
_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);
|
|
}
|
|
|
|
// 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.");
|
|
// Compatibility carrier only. Production visibility is populated
|
|
// exclusively by RetailFrameWalk below; this frame supplies the clip
|
|
// assembler's outdoor full-screen seed until that carrier is removed.
|
|
PortalVisibilityFrame pvFrame = _mainPortalFrameScratch;
|
|
pvFrame.ResetForBuild();
|
|
if (ctx.RootCell.IsOutdoorNode)
|
|
{
|
|
pvFrame.OutsideView.SetFullScreen();
|
|
pvFrame.OrderedVisibleCells.Add(ctx.RootCell.CellId);
|
|
}
|
|
|
|
var clipAssembly = passes.AssembleClipFrame(
|
|
pvFrame,
|
|
_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();
|
|
_drawableCellsScratch.UnionWith(pvFrame.OrderedVisibleCells);
|
|
var drawableCells = _drawableCellsScratch;
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
|
|
// 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 = null;
|
|
WalkProductionLeafRenderer walkLeafRenderer;
|
|
{
|
|
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;
|
|
var walkContext = new Walk.WalkProductionFrameContext(
|
|
_walkCellRegistry!,
|
|
_walkBuildings!,
|
|
ctx.ViewerEyePos,
|
|
forward,
|
|
ctx.ViewProjection,
|
|
viewportWidth,
|
|
viewportHeight);
|
|
_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);
|
|
|
|
Action clearInteriorDepth = () =>
|
|
{
|
|
// FW3 visual-gate fix (owner report: doors/candles invisible
|
|
// looking out; the crossing vanish): retail draws the
|
|
// OUTSIDE world's objects INSIDE LScape::draw — strictly
|
|
// BEFORE the depth clear + seals (the #118 house-exit
|
|
// clip+vanish lesson: anything drawn after the seals z-fails
|
|
// against their true-depth stamp the moment it stands beyond
|
|
// the door plane). The surviving dynamic routes + outdoor
|
|
// particles + weather therefore run HERE, at the walk's
|
|
// pre-clear boundary, for an interior root.
|
|
_walkPreClearDynamics?.Invoke();
|
|
// Retail PView::DrawCells 0x005A4872 drains the landscape
|
|
// alpha list immediately before the gated full depth clear —
|
|
// mirrors DrawLandscapeThroughOutsideView's own pre-clear
|
|
// drain.
|
|
passes.FlushLandscapeAlpha();
|
|
passes.ClearInteriorDepth();
|
|
};
|
|
// FW4 slice 2: the seals stamp the WALK'S OWN flood cells (see
|
|
// DrawWalkExitPortalMasks). walkDriver is assigned below, before
|
|
// any Replay can fire this closure.
|
|
Action drawExitSeals = () =>
|
|
DrawWalkExitPortalMasks(ctx, passes, clipAssembly, walkDriver!);
|
|
|
|
walkLeafRenderer = new WalkProductionLeafRenderer(
|
|
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
|
|
walkDriver = new Walk.WalkFrameDriver(
|
|
walkExecutor!.Dispatcher,
|
|
walkLeafRenderer,
|
|
_walkWorldData);
|
|
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
|
{
|
|
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame =
|
|
_probeWalkRootFrame % 90 == 0;
|
|
}
|
|
walkDriver.Collect(
|
|
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
|
|
ctx.ViewProjection, ctx.CameraWorldPosition);
|
|
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = false;
|
|
|
|
// 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);
|
|
|
|
// Phase I cathedral instrumentation (synthesis §Phase I.3): the
|
|
// continuous rooting line SEPARATES the true root flood
|
|
// (InteriorFloodCells) from the visited union (root + look-ins —
|
|
// the conflation the review corpus indicted), and adds the
|
|
// walk's OWN exit-view count. [walk-cam] dumps a replayable
|
|
// camera every ~300 frames and at every root flip.
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
|
{
|
|
_probeWalkRootFrame++;
|
|
bool outdoorNow = ctx.RootCell.IsOutdoorNode;
|
|
bool flipped = _probeWalkRootPrevOutdoor is bool prev && prev != outdoorNow;
|
|
if (flipped)
|
|
{
|
|
_probeWalkRootFramesLeft = 8;
|
|
Console.WriteLine(
|
|
$"[walk-root] ---- FLIP {(outdoorNow ? "IN->OUT" : "OUT->IN")} at frame {_probeWalkRootFrame} ----");
|
|
}
|
|
_probeWalkRootPrevOutdoor = outdoorNow;
|
|
bool sample = _probeWalkRootFramesLeft > 0 || _probeWalkRootFrame % 30 == 0;
|
|
if (_probeWalkRootFramesLeft > 0)
|
|
_probeWalkRootFramesLeft--;
|
|
if (sample)
|
|
{
|
|
List<uint> rootFlood = walkDriver.InteriorFloodCells;
|
|
string flood = string.Join(",", rootFlood
|
|
.GetRange(0, Math.Min(rootFlood.Count, 8))
|
|
.ConvertAll(c => c.ToString("x8")));
|
|
int walkOv = outdoorNow ? -1 : _frameWalk.InteriorOutsideView.ViewCount;
|
|
Vector3 fwd = walkContext.CyPlane.Normal;
|
|
Console.WriteLine(
|
|
$"[walk-root] f={_probeWalkRootFrame} out={(outdoorNow ? 1 : 0)} "
|
|
+ $"viewer=0x{ctx.ViewerCellId:X8} root=0x{ctx.RootCell.CellId:X8} "
|
|
+ $"res={ctx.CameraCellResolution} slices={clipAssembly.OutsideViewSlices.Length} "
|
|
+ $"walkOv={walkOv} rootFlood={rootFlood.Count}[{flood}] "
|
|
+ $"visited={walkDriver.VisitedCells.Count} bld={walkDriver.VisitedBuildings.Count} "
|
|
+ $"eye=({ctx.ViewerEyePos.X:F2},{ctx.ViewerEyePos.Y:F2},{ctx.ViewerEyePos.Z:F2}) "
|
|
+ $"fwd=({fwd.X:F3},{fwd.Y:F3},{fwd.Z:F3})");
|
|
}
|
|
if (flipped || _probeWalkRootFrame % 300 == 0)
|
|
{
|
|
Matrix4x4 vp = ctx.ViewProjection;
|
|
Console.WriteLine(
|
|
$"[walk-cam] viewer=0x{ctx.ViewerCellId:X8} "
|
|
+ $"eye=({ctx.ViewerEyePos.X:R},{ctx.ViewerEyePos.Y:R},{ctx.ViewerEyePos.Z:R}) "
|
|
+ $"vw={viewportWidth:R} vh={viewportHeight:R} vp=["
|
|
+ $"{vp.M11:R},{vp.M12:R},{vp.M13:R},{vp.M14:R},"
|
|
+ $"{vp.M21:R},{vp.M22:R},{vp.M23:R},{vp.M24:R},"
|
|
+ $"{vp.M31:R},{vp.M32:R},{vp.M33:R},{vp.M34:R},"
|
|
+ $"{vp.M41:R},{vp.M42:R},{vp.M43:R},{vp.M44:R}]");
|
|
}
|
|
}
|
|
}
|
|
|
|
// FW4 slice 1: the ONE clip-region publication, after any walk
|
|
// reassembly so the walk-derived outside-view slots are included
|
|
// (moved from directly after AssembleClipFrame; the count is
|
|
// reservation metadata the RHI arm ignores).
|
|
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
|
passes.PrepareClipFrame(terrainUploadCount);
|
|
|
|
// Production prepares exactly the one walk's visited-cell set.
|
|
HashSet<uint> prepareCells = drawableCells;
|
|
|
|
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
|
|
// here was the seam-floor flicker mechanism — retail's visible_cell_table is
|
|
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
|
// is built once per frame in GameWindow, player-anchored.)
|
|
|
|
passes.PrepareCellBatches(ctx, prepareCells);
|
|
|
|
{
|
|
RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts;
|
|
RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts);
|
|
RenderProjectionCounts sourceCounts = retainedCounts;
|
|
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
|
pvFrame,
|
|
clipAssembly,
|
|
drawableCells,
|
|
prepareCells,
|
|
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 = () =>
|
|
{
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
DrawLandscapeDynamicsPhase(
|
|
ctx,
|
|
passes,
|
|
clipAssembly);
|
|
};
|
|
try
|
|
{
|
|
DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!);
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
if (ctx.RootCell.IsOutdoorNode)
|
|
{
|
|
DrawLandscapeDynamicsPhase(
|
|
ctx,
|
|
passes,
|
|
clipAssembly);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_walkPreClearDynamics = null;
|
|
}
|
|
|
|
// 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>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();
|
|
passes.SetWalkTerrainInViewLandcells(driver.VisitedLandscapeCellIds);
|
|
try
|
|
{
|
|
driver.Replay(frame, encoder);
|
|
}
|
|
finally
|
|
{
|
|
passes.SetWalkTerrainInViewLandcells(null);
|
|
}
|
|
|
|
// 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.
|
|
|
|
// Cell-stage particle owners for the interior root's OWN flood cells
|
|
// (non-look-in — the walk draws their statics too, via
|
|
// OnInteriorFloodDrawTurn/EmitCellTurn, so the retired CellStatic
|
|
// route's cell-particle submission needs the same re-sourcing).
|
|
// These stay POST-replay: interior emitters draw in the final world
|
|
// scope where the cell walls already own depth (retail's cell-walk
|
|
// insertion). Look-in cells get their OWN per-cell union in
|
|
// DrawBuildingLookInDynamics so a static owner is never submitted
|
|
// twice.
|
|
_cellParticleOwnerScratch.Clear();
|
|
foreach (uint cellId in driver.VisitedCells)
|
|
{
|
|
if (driver.LookInCells.Contains(cellId))
|
|
continue;
|
|
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
|
}
|
|
if (_cellParticleOwnerScratch.Count > 0)
|
|
{
|
|
passes.DrawCellParticles(
|
|
ctx,
|
|
new RetailPViewCellSliceContext(0u, NoClipSlice, _cellParticleOwnerScratch));
|
|
}
|
|
}
|
|
|
|
private static void UnionRecordOwners(
|
|
Walk.WalkFrameStaticRecords records, HashSet<uint> destination)
|
|
{
|
|
foreach (RenderProjectionRecord record in records.Records)
|
|
{
|
|
if (record.Source.LocalEntityId != 0)
|
|
destination.Add(record.Source.LocalEntityId);
|
|
}
|
|
}
|
|
|
|
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
|
/// <see cref="DrawLandscapeThroughOutsideView"/> + <see cref="DrawBuildingLookIns"/>
|
|
/// split. The walk now draws every 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)
|
|
{
|
|
if (clipAssembly.OutsideViewSlices.Length == 0)
|
|
return;
|
|
|
|
// Ownerless OUTDOOR-cell emitters — now unconditional: the old
|
|
// hasBuildingLookIns gate only existed to sequence this submission
|
|
// around the OLD static barrier drains, which no longer run here
|
|
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
|
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
|
|
|
// GameSky's weather pass still runs through each active landscape
|
|
// view so doorway scissor/clip state matches the old executor. The
|
|
// dynamics collection is intentionally empty: the walk already drew
|
|
// those records at OnLandscapeCellTurn.
|
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
|
{
|
|
passes.SetTerrainClip(slice.Planes);
|
|
passes.ClearClipRouting();
|
|
passes.DrawLandscapeSliceLate(
|
|
ctx,
|
|
new RetailPViewLandscapeLateSliceContext(
|
|
slice,
|
|
Array.Empty<WorldEntity>()));
|
|
}
|
|
|
|
passes.UseIndoorMembershipOnlyRouting();
|
|
}
|
|
|
|
/// <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,
|
|
/// probe-pinned via ACDREAM_PROBE_WALK_ROOT's phase tags). Per-cell
|
|
/// slice clips still come from the old assembly where present; a cell
|
|
/// the old apparatus missed seals unclipped (the depth fan is the exact
|
|
/// dat aperture polygon and z-tests, so over-coverage is benign).</summary>
|
|
private void DrawWalkExitPortalMasks(
|
|
RetailPViewFrameInput ctx,
|
|
RetailPViewPassExecutor passes,
|
|
ClipFrameAssembly clipAssembly,
|
|
Walk.WalkFrameDriver driver)
|
|
{
|
|
List<uint> floodCells = driver.InteriorFloodCells;
|
|
for (int i = floodCells.Count - 1; i >= 0; i--)
|
|
{
|
|
uint cellId = floodCells[i];
|
|
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
|
passes.DrawExitPortalMask(
|
|
ctx,
|
|
new RetailPViewCellSliceContext(
|
|
cellId,
|
|
slice,
|
|
NoParticleOwners));
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private static ClipViewSlice[] GetCellSlicesOrNoClip(
|
|
ClipFrameAssembly clipAssembly,
|
|
uint cellId)
|
|
{
|
|
if (clipAssembly.CellIdToViewSlices.TryGetValue(cellId, out var slices)
|
|
&& slices.Length > 0)
|
|
{
|
|
return slices;
|
|
}
|
|
|
|
return NoClipSlices;
|
|
}
|
|
}
|
|
|
|
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 PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
|
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(
|
|
PortalVisibilityFrame portalFrame,
|
|
ClipFrameAssembly clipAssembly,
|
|
HashSet<uint> drawableCells,
|
|
HashSet<uint> visibleCells,
|
|
RenderFrameDiagnosticCounts diagnosticCounts,
|
|
RenderProjectionCounts sourceCounts,
|
|
InteriorEntityPartition.Result? diagnosticPartition)
|
|
{
|
|
PortalFrame = portalFrame;
|
|
ClipAssembly = clipAssembly;
|
|
DrawableCells = drawableCells;
|
|
VisibleCells = visibleCells;
|
|
DiagnosticCounts = diagnosticCounts;
|
|
SourceCounts = sourceCounts;
|
|
DiagnosticPartition = diagnosticPartition;
|
|
return this;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scene-particle owners for ONE unclipped landscape-stage submission (the
|
|
/// union of every outside slice's cone survivors). Mesh alpha for the same
|
|
/// owners is already queued by the entity routes; retail inserts each
|
|
/// emitter's polys into the single alpha list once, during its owner cell's
|
|
/// walk turn, with no portal-view clip.
|
|
/// </summary>
|
|
public readonly record struct RetailPViewLandscapeStaticParticleContext(
|
|
IReadOnlySet<uint> ParticleOwnerIds);
|
|
|
|
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
|
/// outside-stage dynamics to mesh-draw, plus the particle owners not already
|
|
/// submitted at a pre-building barrier.</summary>
|
|
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
|
ClipViewSlice Slice,
|
|
IReadOnlyList<WorldEntity> Dynamics);
|
|
|
|
public readonly record struct RetailPViewCellSliceContext(
|
|
uint CellId,
|
|
ClipViewSlice Slice,
|
|
IReadOnlySet<uint> ParticleOwnerIds);
|