checkpoint: preserve user-gated FW closeout fixes
This commit is contained in:
parent
a2f2eb7d78
commit
b8befded8b
22 changed files with 1005 additions and 198 deletions
|
|
@ -202,11 +202,11 @@ internal sealed partial class RetailPViewPassExecutor
|
|||
/// </summary>
|
||||
internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||
{
|
||||
private readonly RetailPViewPassExecutor _passes;
|
||||
private readonly RetailPViewFrameInput _frame;
|
||||
private readonly ClipFrameAssembly _clipAssembly;
|
||||
private readonly Action _clearInteriorDepth;
|
||||
private readonly Action _drawExitSeals;
|
||||
private RetailPViewPassExecutor _passes = null!;
|
||||
private RetailPViewFrameInput _frame = null!;
|
||||
private ClipFrameAssembly _clipAssembly = null!;
|
||||
private Action _clearInteriorDepth = null!;
|
||||
private Action _drawExitSeals = null!;
|
||||
private readonly HashSet<uint> _singleCellScratch = new();
|
||||
private readonly List<uint> _singleCellListScratch = new();
|
||||
private readonly Dictionary<uint, int> _singleCellClipScratch = new(1);
|
||||
|
|
@ -217,10 +217,24 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
ClipFrameAssembly clipAssembly,
|
||||
Action clearInteriorDepth,
|
||||
Action drawExitSeals)
|
||||
=> Reset(passes, frame, clipAssembly, clearInteriorDepth, drawExitSeals);
|
||||
|
||||
/// <summary>
|
||||
/// FW6 allocation closeout: bind the retained leaf and its retained
|
||||
/// one-cell collections to the current frame. Replay is synchronous, so
|
||||
/// no frame may outlive these references.
|
||||
/// </summary>
|
||||
internal void Reset(
|
||||
RetailPViewPassExecutor passes,
|
||||
RetailPViewFrameInput frame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
Action clearInteriorDepth,
|
||||
Action drawExitSeals)
|
||||
{
|
||||
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
||||
_frame = frame ?? throw new ArgumentNullException(nameof(frame));
|
||||
_clipAssembly = clipAssembly;
|
||||
_clipAssembly = clipAssembly
|
||||
?? throw new ArgumentNullException(nameof(clipAssembly));
|
||||
_clearInteriorDepth = clearInteriorDepth
|
||||
?? throw new ArgumentNullException(nameof(clearInteriorDepth));
|
||||
_drawExitSeals = drawExitSeals
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -29,6 +30,7 @@ internal sealed class RetailPViewRenderer
|
|||
// 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
|
||||
|
|
@ -37,6 +39,20 @@ internal sealed class RetailPViewRenderer
|
|||
// cleared in finally.
|
||||
private Action? _walkPreClearDynamics;
|
||||
|
||||
// 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 _walkClearInteriorDepthAction;
|
||||
private readonly Action _walkDrawExitSealsAction;
|
||||
private RetailPViewPassExecutor? _activeWalkPasses;
|
||||
private RetailPViewFrameInput? _activeWalkFrame;
|
||||
private ClipFrameAssembly? _activeWalkClipAssembly;
|
||||
|
||||
// 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.
|
||||
|
|
@ -62,7 +78,8 @@ internal sealed class RetailPViewRenderer
|
|||
RenderSceneShadowRuntime renderSceneShadow,
|
||||
Walk.WalkBuildingRegistry walkBuildings,
|
||||
Walk.WalkLandscapeAssembler walkLandscape,
|
||||
CellVisibility walkCellRegistry)
|
||||
CellVisibility walkCellRegistry,
|
||||
ShadowObjectRegistry shadows)
|
||||
{
|
||||
_renderSceneShadow = renderSceneShadow
|
||||
?? throw new ArgumentNullException(nameof(renderSceneShadow));
|
||||
|
|
@ -72,7 +89,11 @@ internal sealed class RetailPViewRenderer
|
|||
?? throw new ArgumentNullException(nameof(walkLandscape));
|
||||
_walkCellRegistry = walkCellRegistry
|
||||
?? throw new ArgumentNullException(nameof(walkCellRegistry));
|
||||
_walkWorldData = new Walk.WalkProductionWorldData(_walkBuildings);
|
||||
_walkWorldData = new Walk.WalkProductionWorldData(
|
||||
_walkBuildings,
|
||||
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
||||
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
||||
_walkDrawExitSealsAction = DrawWalkExitSeals;
|
||||
}
|
||||
|
||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||
|
|
@ -129,8 +150,7 @@ internal sealed class RetailPViewRenderer
|
|||
// 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;
|
||||
Walk.WalkFrameDriver walkDriver;
|
||||
{
|
||||
Matrix4x4 view = ctx.CameraView;
|
||||
var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33));
|
||||
|
|
@ -140,14 +160,27 @@ internal sealed class RetailPViewRenderer
|
|||
// 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);
|
||||
if (_walkFrameContextScratch is null)
|
||||
{
|
||||
_walkFrameContextScratch = new Walk.WalkProductionFrameContext(
|
||||
_walkCellRegistry,
|
||||
_walkBuildings,
|
||||
ctx.ViewerEyePos,
|
||||
forward,
|
||||
ctx.ViewProjection,
|
||||
viewportWidth,
|
||||
viewportHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
_walkFrameContextScratch.Reset(
|
||||
ctx.ViewerEyePos,
|
||||
forward,
|
||||
ctx.ViewProjection,
|
||||
viewportWidth,
|
||||
viewportHeight);
|
||||
}
|
||||
Walk.WalkProductionFrameContext walkContext = _walkFrameContextScratch;
|
||||
_walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos);
|
||||
Walk.WalkLandscape walkLandscape = _walkLandscape.Landscape;
|
||||
|
||||
|
|
@ -185,47 +218,60 @@ internal sealed class RetailPViewRenderer
|
|||
ctx.RenderCenterLbX,
|
||||
ctx.RenderCenterLbY);
|
||||
|
||||
Action clearInteriorDepth = () =>
|
||||
_activeWalkPasses = walkExecutor;
|
||||
_activeWalkFrame = ctx;
|
||||
_activeWalkClipAssembly = clipAssembly;
|
||||
if (_walkLeafRendererScratch is null)
|
||||
{
|
||||
// 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!);
|
||||
_walkLeafRendererScratch = new WalkProductionLeafRenderer(
|
||||
walkExecutor,
|
||||
ctx,
|
||||
clipAssembly,
|
||||
_walkClearInteriorDepthAction,
|
||||
_walkDrawExitSealsAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
_walkLeafRendererScratch.Reset(
|
||||
walkExecutor,
|
||||
ctx,
|
||||
clipAssembly,
|
||||
_walkClearInteriorDepthAction,
|
||||
_walkDrawExitSealsAction);
|
||||
}
|
||||
|
||||
walkLeafRenderer = new WalkProductionLeafRenderer(
|
||||
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
|
||||
walkDriver = new Walk.WalkFrameDriver(
|
||||
walkExecutor!.Dispatcher,
|
||||
walkLeafRenderer,
|
||||
_walkWorldData,
|
||||
clipFrame: clipAssembly.Frame);
|
||||
if (_walkFrameDriverScratch is null)
|
||||
{
|
||||
_walkFrameDriverScratch = new Walk.WalkFrameDriver(
|
||||
walkExecutor.Dispatcher,
|
||||
_walkLeafRendererScratch,
|
||||
_walkWorldData,
|
||||
clipFrame: clipAssembly.Frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
_walkFrameDriverScratch.RebindFrame(
|
||||
_walkLeafRendererScratch,
|
||||
clipAssembly.Frame);
|
||||
}
|
||||
walkDriver = _walkFrameDriverScratch;
|
||||
|
||||
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);
|
||||
try
|
||||
{
|
||||
walkDriver.Collect(
|
||||
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
|
||||
ctx.ViewProjection, ctx.CameraWorldPosition);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ClearWalkFrameBindings();
|
||||
throw;
|
||||
}
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = false;
|
||||
|
||||
// FW4 slice 1: an interior root's terrain/sky/punch clip slices
|
||||
|
|
@ -248,6 +294,7 @@ internal sealed class RetailPViewRenderer
|
|||
// OrderedVisibleCells side-channel for every production consumer.
|
||||
_drawableCellsScratch.Clear();
|
||||
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
|
||||
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
|
||||
|
||||
// Phase I cathedral instrumentation (synthesis §Phase I.3): the
|
||||
// continuous rooting line SEPARATES the true root flood
|
||||
|
|
@ -326,7 +373,7 @@ internal sealed class RetailPViewRenderer
|
|||
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
||||
clipAssembly,
|
||||
drawableCells,
|
||||
prepareCells,
|
||||
_visibleCellsScratch,
|
||||
counts,
|
||||
sourceCounts,
|
||||
diagnosticPartition: null);
|
||||
|
|
@ -360,6 +407,7 @@ internal sealed class RetailPViewRenderer
|
|||
finally
|
||||
{
|
||||
_walkPreClearDynamics = null;
|
||||
ClearWalkFrameBindings();
|
||||
}
|
||||
|
||||
// OUTDOOR root: the LScape-boundary alpha drain deferred from the
|
||||
|
|
@ -386,6 +434,46 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
private readonly Walk.RetailFrameWalk _frameWalk = new();
|
||||
|
||||
private void ClearWalkInteriorDepth()
|
||||
{
|
||||
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();
|
||||
passes.ClearInteriorDepth();
|
||||
}
|
||||
|
||||
private void 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.");
|
||||
ClipFrameAssembly clipAssembly = _activeWalkClipAssembly
|
||||
?? throw new InvalidOperationException(
|
||||
"The retained walk leaf has no active clip binding.");
|
||||
Walk.WalkFrameDriver driver = _walkFrameDriverScratch
|
||||
?? throw new InvalidOperationException(
|
||||
"The retained walk leaf has no active driver binding.");
|
||||
|
||||
DrawWalkExitPortalMasks(frame, passes, clipAssembly, 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>);
|
||||
|
|
|
|||
|
|
@ -465,10 +465,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
{
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly WalkStaticStreamPopulator _populator;
|
||||
private readonly IWalkFrameLeafRenderer _leafRenderer;
|
||||
private IWalkFrameLeafRenderer _leafRenderer;
|
||||
private readonly IWalkFrameWorldData _worldData;
|
||||
private readonly IWalkFrameDriverTrace? _trace;
|
||||
private readonly ClipFrame? _clipFrame;
|
||||
private ClipFrame? _clipFrame;
|
||||
private readonly OrderedDrawStream _stream = new();
|
||||
private readonly List<WalkFrameEvent> _events = new();
|
||||
private readonly List<int> _markPositions = new();
|
||||
|
|
@ -493,6 +493,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private readonly List<int> _floodViewRouteScratch = new();
|
||||
private WalkPlane _lookInCyPlane;
|
||||
|
||||
// Retail adds each physics object to every overlapped cell's shadow-part
|
||||
// list, then CPhysicsPart::Get/SetDrawnThisFrame (0x0059F388) prevents
|
||||
// those aliases from drawing more than once. The walk's outdoor buckets
|
||||
// now carry the same multi-cell aliases, so retain the same frame guard.
|
||||
private readonly HashSet<RenderProjectionId> _outdoorDrawnThisFrame = new();
|
||||
private readonly HashSet<uint> _outdoorParticleOwnersDrawnThisFrame = new();
|
||||
|
||||
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
||||
|
||||
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
|
||||
|
|
@ -524,6 +531,19 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
}
|
||||
}
|
||||
|
||||
private static void UnionNewOwners(
|
||||
in WalkFrameStaticRecords records,
|
||||
HashSet<uint> destination,
|
||||
HashSet<uint> drawnOnce)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
{
|
||||
uint ownerId = record.Source.LocalEntityId;
|
||||
if (ownerId != 0 && drawnOnce.Add(ownerId))
|
||||
destination.Add(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
internal List<WalkBuilding> VisitedBuildings { get; } = new();
|
||||
|
||||
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
|
||||
|
|
@ -554,6 +574,77 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_populator = new WalkStaticStreamPopulator(dispatcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FW6 allocation closeout: reuse this driver's large event/view/stream
|
||||
/// scratch across production frames while rebinding the frame-local leaf
|
||||
/// and clip destination. A prior failed frame is discarded here so the
|
||||
/// renderer's report-and-continue policy cannot poison the next frame.
|
||||
/// </summary>
|
||||
internal void RebindFrame(
|
||||
IWalkFrameLeafRenderer leafRenderer,
|
||||
ClipFrame? clipFrame)
|
||||
{
|
||||
AbortFrame();
|
||||
_leafRenderer = leafRenderer
|
||||
?? throw new ArgumentNullException(nameof(leafRenderer));
|
||||
_clipFrame = clipFrame;
|
||||
}
|
||||
|
||||
/// <summary>Discard only transient frame state; retained capacities stay
|
||||
/// available for the next frame.</summary>
|
||||
internal void AbortFrame()
|
||||
{
|
||||
_ctx = null;
|
||||
_viewProjection = default;
|
||||
_cameraWorldPosition = default;
|
||||
_skyDrawnThisFrame = false;
|
||||
_currentDcStage = null;
|
||||
_readyToReplay = false;
|
||||
_stream.Reset();
|
||||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
VisitedCells.Clear();
|
||||
LookInCellTurns.Clear();
|
||||
_lookInTurns.Clear();
|
||||
_lookInSlices.Clear();
|
||||
_lookInPlanes.Clear();
|
||||
_visibleClipSlotScratch.Clear();
|
||||
_floodViewRouteScratch.Clear();
|
||||
_outdoorDrawnThisFrame.Clear();
|
||||
_outdoorParticleOwnersDrawnThisFrame.Clear();
|
||||
_lookInCyPlane = default;
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
InteriorFloodCells.Clear();
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
_cellViewRouteIndex = 0;
|
||||
_landscapeViewRouteIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the walk's complete retail <c>CObjCell::IsInView</c> answer:
|
||||
/// EnvCells reached by interior floods/look-ins plus outdoor land cells
|
||||
/// visited by the landscape walk. The two families stay separately
|
||||
/// retained because only the first is valid EnvCell batch input, but
|
||||
/// particles, lights, and shadows consume their union.
|
||||
/// </summary>
|
||||
internal void CopyVisibleCellsTo(HashSet<uint> destination)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
if (ReferenceEquals(destination, VisitedCells)
|
||||
|| ReferenceEquals(destination, VisitedLandscapeCellIds))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The visible-cell destination cannot alias a walk source set.",
|
||||
nameof(destination));
|
||||
}
|
||||
|
||||
destination.Clear();
|
||||
destination.UnionWith(VisitedCells);
|
||||
destination.UnionWith(VisitedLandscapeCellIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drives one complete frame at retail's root (<c>SmartBox::RenderNormalMode</c>):
|
||||
/// <see cref="Collect"/> immediately followed by <see cref="Replay"/>. Kept
|
||||
|
|
@ -601,8 +692,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
|
||||
BeginFrame(ctx, viewProjection, cameraWorldPosition);
|
||||
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
|
||||
EndFrame();
|
||||
try
|
||||
{
|
||||
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
|
||||
EndFrame();
|
||||
}
|
||||
catch
|
||||
{
|
||||
AbortFrame();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -644,6 +743,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_lookInPlanes.Clear();
|
||||
_visibleClipSlotScratch.Clear();
|
||||
_lookInCyPlane = ctx.CyPlane;
|
||||
_outdoorDrawnThisFrame.Clear();
|
||||
_outdoorParticleOwnersDrawnThisFrame.Clear();
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
|
|
@ -697,82 +798,94 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
+ "replay.");
|
||||
}
|
||||
|
||||
if (_stream.Count > 0)
|
||||
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
|
||||
|
||||
int cursor = 0;
|
||||
for (int i = 0; i < _events.Count; i++)
|
||||
try
|
||||
{
|
||||
WalkFrameEvent e = _events[i];
|
||||
switch (e.Kind)
|
||||
if (_stream.Count > 0)
|
||||
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
|
||||
|
||||
int cursor = 0;
|
||||
for (int i = 0; i < _events.Count; i++)
|
||||
{
|
||||
case WalkFrameEventKind.StreamMark:
|
||||
int end = e.IntArg;
|
||||
int count = end - cursor;
|
||||
if (_trace is not null)
|
||||
_trace.OnFlush(count, _stream.Stages.GetRange(cursor, count));
|
||||
_dispatcher.DrawOrderedRange(encoder, cursor, count);
|
||||
cursor = end;
|
||||
break;
|
||||
case WalkFrameEventKind.Sky:
|
||||
_leafRenderer.DrawSky();
|
||||
break;
|
||||
case WalkFrameEventKind.TerrainSlice:
|
||||
_leafRenderer.DrawTerrainSlice(e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.CellShell:
|
||||
_leafRenderer.DrawCellShell(e.CellId, checked((uint)e.IntArg));
|
||||
break;
|
||||
case WalkFrameEventKind.PunchFan:
|
||||
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaBarrier:
|
||||
_leafRenderer.AlphaBarrier();
|
||||
break;
|
||||
case WalkFrameEventKind.ClearInteriorDepth:
|
||||
_leafRenderer.ClearInteriorDepth();
|
||||
break;
|
||||
case WalkFrameEventKind.ExitSeals:
|
||||
_leafRenderer.DrawExitSeals();
|
||||
break;
|
||||
case WalkFrameEventKind.StaticParticles:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
UnionOwners(
|
||||
e.Building is WalkBuilding shellOwner
|
||||
? _worldData.GetBuildingShellStatics(shellOwner)
|
||||
: _worldData.GetOutdoorStatics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
if (e.Building is null)
|
||||
{
|
||||
WalkFrameEvent e = _events[i];
|
||||
switch (e.Kind)
|
||||
{
|
||||
case WalkFrameEventKind.StreamMark:
|
||||
int end = e.IntArg;
|
||||
int count = end - cursor;
|
||||
if (_trace is not null)
|
||||
_trace.OnFlush(count, _stream.Stages.GetRange(cursor, count));
|
||||
_dispatcher.DrawOrderedRange(encoder, cursor, count);
|
||||
cursor = end;
|
||||
break;
|
||||
case WalkFrameEventKind.Sky:
|
||||
_leafRenderer.DrawSky();
|
||||
break;
|
||||
case WalkFrameEventKind.TerrainSlice:
|
||||
_leafRenderer.DrawTerrainSlice(e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.CellShell:
|
||||
_leafRenderer.DrawCellShell(e.CellId, checked((uint)e.IntArg));
|
||||
break;
|
||||
case WalkFrameEventKind.PunchFan:
|
||||
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaBarrier:
|
||||
_leafRenderer.AlphaBarrier();
|
||||
break;
|
||||
case WalkFrameEventKind.ClearInteriorDepth:
|
||||
_leafRenderer.ClearInteriorDepth();
|
||||
break;
|
||||
case WalkFrameEventKind.ExitSeals:
|
||||
_leafRenderer.DrawExitSeals();
|
||||
break;
|
||||
case WalkFrameEventKind.StaticParticles:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
if (e.Building is WalkBuilding shellOwner)
|
||||
{
|
||||
UnionOwners(
|
||||
_worldData.GetBuildingShellStatics(shellOwner),
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnionNewOwners(
|
||||
_worldData.GetOutdoorStatics(e.CellId),
|
||||
_staticParticleOwnerScratch,
|
||||
_outdoorParticleOwnersDrawnThisFrame);
|
||||
UnionNewOwners(
|
||||
_worldData.GetOutdoorDynamics(e.CellId),
|
||||
_staticParticleOwnerScratch,
|
||||
_outdoorParticleOwnersDrawnThisFrame);
|
||||
}
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||
break;
|
||||
case WalkFrameEventKind.CellParticles:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
UnionOwners(
|
||||
_worldData.GetOutdoorDynamics(e.CellId),
|
||||
_worldData.GetCellStatics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||
break;
|
||||
case WalkFrameEventKind.CellParticles:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
UnionOwners(
|
||||
_worldData.GetCellStatics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
UnionOwners(
|
||||
_worldData.GetCellDynamics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
_leafRenderer.DrawCellParticles(
|
||||
e.CellId,
|
||||
UnionOwners(
|
||||
_worldData.GetCellDynamics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
break;
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
_leafRenderer.DrawCellParticles(
|
||||
e.CellId,
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_stream.Reset();
|
||||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
_readyToReplay = false;
|
||||
finally
|
||||
{
|
||||
_stream.Reset();
|
||||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
_readyToReplay = false;
|
||||
_ctx = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -817,11 +930,11 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_populator.PopulateOutdoorStatics(
|
||||
_stream, cellId, records.Records, records.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, _landscapeViewRouteIndex);
|
||||
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame);
|
||||
_populator.PopulateCellDynamics(
|
||||
_stream, cellId, dynamics.Records, dynamics.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, _landscapeViewRouteIndex);
|
||||
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame);
|
||||
// FW4 (the #132 positional invariant): this cell's emitter owners
|
||||
// submit AT THIS TURN, so nearer buildings' pre-punch barriers
|
||||
// drain them against still-true depth — see
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ public sealed class WalkLandscape
|
|||
public int ViewerCellX; // viewer cell & 7 per axis (SqCoord)
|
||||
public int ViewerCellY;
|
||||
|
||||
/// <summary>The viewer landblock's southwest corner in the current
|
||||
/// render-center coordinate system. The landscape grid is indexed around
|
||||
/// the viewer's DAT landblock, while camera/portal planes are expressed
|
||||
/// relative to the independently moving render center. Those bases differ
|
||||
/// by whole 192 m blocks whenever the render center trails the camera.</summary>
|
||||
public float ViewerWorldOriginX;
|
||||
public float ViewerWorldOriginY;
|
||||
|
||||
public int[] BlockDrawList = [];
|
||||
public int BlockDrawCount;
|
||||
|
||||
|
|
@ -154,8 +162,8 @@ public sealed class WalkLandscape
|
|||
// Seed the west column (grid x = 0) into parity row 0.
|
||||
for (int j = 0; j <= MidWidth; j++)
|
||||
WalkVisibilityMath.FillClipHeights(
|
||||
(0 - ViewerBlockX) * BlockLength,
|
||||
(j - ViewerBlockY) * BlockLength,
|
||||
ViewerWorldOriginX + (0 - ViewerBlockX) * BlockLength,
|
||||
ViewerWorldOriginY + (j - ViewerBlockY) * BlockLength,
|
||||
cyPlane, edgePlanes, intervals[j]);
|
||||
for (int bx = 0; bx < MidWidth; bx++)
|
||||
{
|
||||
|
|
@ -163,8 +171,8 @@ public sealed class WalkLandscape
|
|||
int eastRow = ((bx - 1) & 1) * cornerRow;
|
||||
for (int j = 0; j <= MidWidth; j++)
|
||||
WalkVisibilityMath.FillClipHeights(
|
||||
(bx + 1 - ViewerBlockX) * BlockLength,
|
||||
(j - ViewerBlockY) * BlockLength,
|
||||
ViewerWorldOriginX + (bx + 1 - ViewerBlockX) * BlockLength,
|
||||
ViewerWorldOriginY + (j - ViewerBlockY) * BlockLength,
|
||||
cyPlane, edgePlanes, intervals[eastRow + j]);
|
||||
for (int by = 0; by < MidWidth; by++)
|
||||
{
|
||||
|
|
@ -205,8 +213,8 @@ public sealed class WalkLandscape
|
|||
block.CellInView[i] = WalkBoundingType.EntirelyInside;
|
||||
return;
|
||||
}
|
||||
float x0 = (bx - ViewerBlockX) * BlockLength;
|
||||
float y0 = (by - ViewerBlockY) * BlockLength;
|
||||
float x0 = ViewerWorldOriginX + (bx - ViewerBlockX) * BlockLength;
|
||||
float y0 = ViewerWorldOriginY + (by - ViewerBlockY) * BlockLength;
|
||||
int cornerRow = n + 1;
|
||||
var grid = new float[2 * cornerRow][];
|
||||
for (int i = 0; i < grid.Length; i++) grid[i] = new float[32];
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ public sealed class WalkLandscapeAssembler
|
|||
int cellIndex = (int)low - 1;
|
||||
Landscape.ViewerCellX = cellIndex / 8;
|
||||
Landscape.ViewerCellY = cellIndex % 8;
|
||||
Landscape.ViewerWorldOriginX = DeriveViewerBlockOrigin(
|
||||
cameraOrigin.X, Landscape.ViewerCellX);
|
||||
Landscape.ViewerWorldOriginY = DeriveViewerBlockOrigin(
|
||||
cameraOrigin.Y, Landscape.ViewerCellY);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -118,11 +122,30 @@ public sealed class WalkLandscapeAssembler
|
|||
// the camera origin, matching the test builder's fallback
|
||||
// (Position::get_outside_cell_id via SmartBox::RenderNormalMode's
|
||||
// seen_outside arm).
|
||||
Landscape.ViewerCellX = Math.Clamp((int)MathF.Floor(cameraOrigin.X / 24f), 0, 7);
|
||||
Landscape.ViewerCellY = Math.Clamp((int)MathF.Floor(cameraOrigin.Y / 24f), 0, 7);
|
||||
Landscape.ViewerWorldOriginX = MathF.Floor(
|
||||
cameraOrigin.X / WalkLandscape.BlockLength) * WalkLandscape.BlockLength;
|
||||
Landscape.ViewerWorldOriginY = MathF.Floor(
|
||||
cameraOrigin.Y / WalkLandscape.BlockLength) * WalkLandscape.BlockLength;
|
||||
Landscape.ViewerCellX = Math.Clamp((int)MathF.Floor(
|
||||
(cameraOrigin.X - Landscape.ViewerWorldOriginX) / WalkLandscape.CellLength), 0, 7);
|
||||
Landscape.ViewerCellY = Math.Clamp((int)MathF.Floor(
|
||||
(cameraOrigin.Y - Landscape.ViewerWorldOriginY) / WalkLandscape.CellLength), 0, 7);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Recover the whole-block render-center offset from the exact
|
||||
/// outdoor cell identity. Using the cell center makes the calculation
|
||||
/// stable at cell edges: every valid position in the authored 24 m cell
|
||||
/// lies within 12 m of the expected center, far from the 96 m rounding
|
||||
/// boundary between possible 192 m block origins.</summary>
|
||||
private static float DeriveViewerBlockOrigin(float cameraAxis, int cellAxis)
|
||||
{
|
||||
float cellCenter = (cellAxis + 0.5f) * WalkLandscape.CellLength;
|
||||
return MathF.Round(
|
||||
(cameraAxis - cellCenter) / WalkLandscape.BlockLength,
|
||||
MidpointRounding.AwayFromZero) * WalkLandscape.BlockLength;
|
||||
}
|
||||
|
||||
/// <summary><c>ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1</c>
|
||||
/// — the live-observed resolution pyramid (recon 2026-08-30): 8×8 in the
|
||||
/// 3×3 core, 4×4 at ring 2, 2×2 at rings 3–4, 1×1 beyond. Buildings only
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ namespace AcDream.App.Rendering.Walk;
|
|||
/// true eye ray (near/far unprojection) are observably equivalent to
|
||||
/// retail's exact construction for this contract.
|
||||
///
|
||||
/// One instance is a per-frame value (like <c>WalkTraceReplayContext</c>):
|
||||
/// construct fresh each frame with that frame's camera pose.
|
||||
/// Production retains one instance per renderer and rebinds its frame-local
|
||||
/// camera values through <see cref="Reset"/>. The registries and grow-only
|
||||
/// active-view scratch remain renderer-lifetime owners.
|
||||
/// </summary>
|
||||
public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrameWalkContext
|
||||
{
|
||||
|
|
@ -35,19 +36,28 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
|
||||
private sealed class InverseViewProjectionRayCaster : IWalkRayCaster
|
||||
{
|
||||
private readonly Matrix4x4 _inverseViewProjection;
|
||||
private readonly float _viewportWidth;
|
||||
private readonly float _viewportHeight;
|
||||
private Matrix4x4 _inverseViewProjection;
|
||||
private float _viewportWidth;
|
||||
private float _viewportHeight;
|
||||
|
||||
public InverseViewProjectionRayCaster(
|
||||
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
|
||||
=> Reset(viewProjection, viewportWidth, viewportHeight);
|
||||
|
||||
internal void Reset(
|
||||
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
|
||||
{
|
||||
if (!Matrix4x4.Invert(viewProjection, out _inverseViewProjection))
|
||||
if (!Matrix4x4.Invert(viewProjection, out Matrix4x4 inverseViewProjection))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The walk's view-projection matrix must be invertible.",
|
||||
nameof(viewProjection));
|
||||
}
|
||||
// Publish only after validation succeeds. Matrix4x4.Invert writes
|
||||
// its out value even on failure; assigning the field directly
|
||||
// would silently corrupt the retained ray caster for the next
|
||||
// report-and-continue frame.
|
||||
_inverseViewProjection = inverseViewProjection;
|
||||
_viewportWidth = viewportWidth;
|
||||
_viewportHeight = viewportHeight;
|
||||
}
|
||||
|
|
@ -72,8 +82,8 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
|
||||
private readonly CellVisibility _cells;
|
||||
private readonly WalkBuildingRegistry _buildings;
|
||||
private readonly Matrix4x4 _viewProjection;
|
||||
private readonly IWalkRayCaster _rays;
|
||||
private Matrix4x4 _viewProjection;
|
||||
private readonly InverseViewProjectionRayCaster _rays;
|
||||
|
||||
private Vector2[] _activeViewVerts = new Vector2[32];
|
||||
private int _activeViewVertCount;
|
||||
|
|
@ -89,19 +99,39 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
{
|
||||
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
|
||||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||||
_rays = new InverseViewProjectionRayCaster(
|
||||
viewProjection, viewportWidth, viewportHeight);
|
||||
Reset(worldViewpoint, forward, viewProjection, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FW6 allocation closeout: rebind this retained context to one frame's
|
||||
/// camera values while preserving its active-view scratch. The cell and
|
||||
/// building registries are lifetime owners and therefore never change.
|
||||
/// </summary>
|
||||
internal void Reset(
|
||||
Vector3 worldViewpoint,
|
||||
Vector3 forward,
|
||||
Matrix4x4 viewProjection,
|
||||
float viewportWidth,
|
||||
float viewportHeight)
|
||||
{
|
||||
// Validate/invert before publishing any new frame value so a bad
|
||||
// camera matrix leaves the previous usable binding intact.
|
||||
_rays.Reset(viewProjection, viewportWidth, viewportHeight);
|
||||
WorldViewpoint = worldViewpoint;
|
||||
_viewProjection = viewProjection;
|
||||
ViewportWidth = viewportWidth;
|
||||
ViewportHeight = viewportHeight;
|
||||
_rays = new InverseViewProjectionRayCaster(viewProjection, viewportWidth, viewportHeight);
|
||||
_activeViewVertCount = 0;
|
||||
// The retail CY near plane: N = forward, d = -dot(eye, forward) - znear.
|
||||
CyPlane = new WalkPlane(forward, -Vector3.Dot(worldViewpoint, forward) - ZNear);
|
||||
}
|
||||
|
||||
public Vector3 WorldViewpoint { get; }
|
||||
public float ViewportWidth { get; }
|
||||
public float ViewportHeight { get; }
|
||||
public WalkPlane CyPlane { get; }
|
||||
public Vector3 WorldViewpoint { get; private set; }
|
||||
public float ViewportWidth { get; private set; }
|
||||
public float ViewportHeight { get; private set; }
|
||||
public WalkPlane CyPlane { get; private set; }
|
||||
public IWalkRayCaster Rays => _rays;
|
||||
public IWalkFrameContext CellContext => this;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Rendering.Walk;
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ namespace AcDream.App.Rendering.Walk;
|
|||
internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
||||
{
|
||||
private readonly WalkBuildingRegistry _buildings;
|
||||
private readonly ShadowObjectRegistry _shadows;
|
||||
private RenderSceneQuery _scene;
|
||||
private uint _tupleLandblockId;
|
||||
private int _renderCenterLbX;
|
||||
|
|
@ -77,9 +79,12 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
private RenderProjectionRecord[] _arena = new RenderProjectionRecord[4096];
|
||||
private int _arenaLength;
|
||||
|
||||
internal WalkProductionWorldData(WalkBuildingRegistry buildings)
|
||||
internal WalkProductionWorldData(
|
||||
WalkBuildingRegistry buildings,
|
||||
ShadowObjectRegistry shadows)
|
||||
{
|
||||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||||
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
|
||||
}
|
||||
|
||||
/// <summary>Rebuilds the frame's outdoor/shell buckets and clears the
|
||||
|
|
@ -136,11 +141,12 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
shells.Add(record);
|
||||
continue;
|
||||
}
|
||||
uint cellId = LandscapeCellId(
|
||||
record.Transform.Position, _renderCenterLbX, _renderCenterLbY);
|
||||
if (!_outdoorByCell.TryGetValue(cellId, out List<RenderProjectionRecord>? bucket))
|
||||
_outdoorByCell[cellId] = bucket = new List<RenderProjectionRecord>();
|
||||
bucket.Add(record);
|
||||
BucketOutdoorRecord(
|
||||
in record,
|
||||
_shadows.GetOwnerCells(record.Source.LocalEntityId),
|
||||
_outdoorByCell,
|
||||
_renderCenterLbX,
|
||||
_renderCenterLbY);
|
||||
}
|
||||
|
||||
required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorDynamic);
|
||||
|
|
@ -155,18 +161,70 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ref readonly RenderProjectionRecord record = ref _dynamicSweepScratch[i];
|
||||
uint cellId = LandscapeCellId(
|
||||
record.Transform.Position, _renderCenterLbX, _renderCenterLbY);
|
||||
if (!_outdoorDynamicsByCell.TryGetValue(
|
||||
cellId,
|
||||
out List<RenderProjectionRecord>? bucket))
|
||||
{
|
||||
_outdoorDynamicsByCell[cellId] = bucket = new List<RenderProjectionRecord>();
|
||||
}
|
||||
bucket.Add(record);
|
||||
BucketOutdoorRecord(
|
||||
in record,
|
||||
_shadows.GetOwnerCells(record.Source.LocalEntityId),
|
||||
_outdoorDynamicsByCell,
|
||||
_renderCenterLbX,
|
||||
_renderCenterLbY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs one outdoor object's render shadow in every outdoor cell of
|
||||
/// its authoritative physics <c>CELLARRAY</c>. This is retail's
|
||||
/// <c>CPhysicsObj::add_shadows_to_cells</c> →
|
||||
/// <c>CPartArray::AddPartsShadow</c> path: a large object straddling a
|
||||
/// landblock edge must remain reachable when its origin cell leaves the
|
||||
/// landscape walk. Objects without a collision registration (notably
|
||||
/// short-lived visual effects) retain the root-position fallback.
|
||||
/// </summary>
|
||||
internal static void BucketOutdoorRecord(
|
||||
in RenderProjectionRecord record,
|
||||
IReadOnlyList<uint> shadowCells,
|
||||
Dictionary<uint, List<RenderProjectionRecord>> buckets,
|
||||
int renderCenterLbX,
|
||||
int renderCenterLbY)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(shadowCells);
|
||||
ArgumentNullException.ThrowIfNull(buckets);
|
||||
|
||||
bool added = false;
|
||||
for (int i = 0; i < shadowCells.Count; i++)
|
||||
{
|
||||
uint cellId = shadowCells[i];
|
||||
uint cellIndex = cellId & 0xFFFFu;
|
||||
if (cellIndex is < 1u or > 64u)
|
||||
continue;
|
||||
|
||||
AddToBucket(in record, cellId, buckets);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (!added)
|
||||
{
|
||||
uint cellId = LandscapeCellId(
|
||||
record.Transform.Position,
|
||||
renderCenterLbX,
|
||||
renderCenterLbY);
|
||||
AddToBucket(in record, cellId, buckets);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddToBucket(
|
||||
in RenderProjectionRecord record,
|
||||
uint cellId,
|
||||
Dictionary<uint, List<RenderProjectionRecord>> buckets)
|
||||
{
|
||||
if (!buckets.TryGetValue(
|
||||
cellId,
|
||||
out List<RenderProjectionRecord>? bucket))
|
||||
{
|
||||
buckets[cellId] = bucket = new List<RenderProjectionRecord>();
|
||||
}
|
||||
bucket.Add(record);
|
||||
}
|
||||
|
||||
/// <summary>The landscape cell owning a RENDER-ORIGIN-RELATIVE position
|
||||
/// — retail's 24 m cell grid inside the 192 m landblock, producing the
|
||||
/// same TRUE <c>(lb & 0xFFFF0000) | (cellX*8 + cellY + 1)</c> encoding
|
||||
|
|
|
|||
|
|
@ -97,11 +97,14 @@ internal sealed class WalkStaticStreamPopulator
|
|||
Vector3 cameraWorldPosition,
|
||||
Matrix4x4 viewProjection,
|
||||
IWalkLookInViewSource? views = null,
|
||||
int viewRouteIndex = -1)
|
||||
int viewRouteIndex = -1,
|
||||
ISet<RenderProjectionId>? drawnOnce = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
if (drawnOnce is not null && !drawnOnce.Add(records[i].Id))
|
||||
continue;
|
||||
ClassifyAndAppend(
|
||||
stream, WalkDrawStage.OutdoorStatic, cellId, in records[i],
|
||||
tupleLandblockId, cameraWorldPosition, viewProjection,
|
||||
|
|
@ -117,11 +120,14 @@ internal sealed class WalkStaticStreamPopulator
|
|||
Vector3 cameraWorldPosition,
|
||||
Matrix4x4 viewProjection,
|
||||
IWalkLookInViewSource? lookInViews = null,
|
||||
int lookInRouteIndex = -1)
|
||||
int lookInRouteIndex = -1,
|
||||
ISet<RenderProjectionId>? drawnOnce = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
if (drawnOnce is not null && !drawnOnce.Add(records[i].Id))
|
||||
continue;
|
||||
ClassifyAndAppend(
|
||||
stream,
|
||||
WalkDrawStage.Dynamic,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,20 @@ public sealed partial class WbDrawDispatcher
|
|||
ArgumentNullException.ThrowIfNull(batches);
|
||||
ArgumentNullException.ThrowIfNull(selectionParts);
|
||||
|
||||
// Retail CPhysicsObj::set_nodraw -> CPartArray::SetNoDrawInternal
|
||||
// removes the PartArray from drawing immediately; logical object,
|
||||
// cell, script, particle, and eventual DeleteObject lifetimes remain
|
||||
// independent. The retained render scene expresses that exact edge
|
||||
// by clearing RenderProjectionFlags.Draw. The pre-FW dispatcher read
|
||||
// WorldEntity.IsDrawVisible before classifying, but the production
|
||||
// frame-walk consumes immutable RenderProjectionRecords and therefore
|
||||
// must honor the equivalent record flag here. Without this gate an
|
||||
// impacted spell projectile remained visible until ACE's delayed
|
||||
// DeleteObject five seconds later even though its SetState had already
|
||||
// set NoDraw.
|
||||
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
||||
return;
|
||||
|
||||
RenderInstanceCandidate entity =
|
||||
RenderInstanceCandidate.FromProjection(
|
||||
in projection,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue