checkpoint: preserve user-gated FW closeout fixes

This commit is contained in:
Erik 2026-08-31 08:27:37 +02:00
parent a2f2eb7d78
commit b8befded8b
22 changed files with 1005 additions and 198 deletions

View file

@ -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

View file

@ -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];

View file

@ -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 &lt;= 1 ? 8 : ring == 2 ? 4 : ring &lt;= 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 34, 1×1 beyond. Buildings only

View file

@ -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;

View file

@ -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 &amp; 0xFFFF0000) | (cellX*8 + cellY + 1)</c> encoding

View file

@ -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,