fix(render): restore landscape objects and walk alpha order

This commit is contained in:
Erik 2026-08-31 10:49:33 +02:00
parent 4683ac6f72
commit e880860291
5 changed files with 275 additions and 21 deletions

View file

@ -177,7 +177,10 @@ public sealed class RetailFrameWalk
// outdoor-static turn.
if (block.CellBuildings[cellIndex] is WalkBuilding building)
DrawBuilding(building, activeViews, ctx, sink);
sink.OnLandscapeCellTurn((block.LandblockId & 0xFFFF0000u) | (uint)(cellIndex + 1));
sink.OnLandscapeCellTurn(
block.LandblockId,
block.SideCellCount,
cellIndex);
}
}
}

View file

@ -99,6 +99,19 @@ public interface IWalkEventSink
/// </summary>
void OnLandscapeCellTurn(uint cellId) { }
/// <summary>
/// The LOD-aware sibling of <see cref="OnLandscapeCellTurn(uint)"/>.
/// <paramref name="cellIndex"/> names one cell in the block's CURRENT
/// <paramref name="sideCellCount"/>×<paramref name="sideCellCount"/>
/// coarse terrain grid. Retail attaches every outdoor object's shadow to
/// that coarse <c>CObjCell</c>; acdream retains the authoritative 8×8
/// owner buckets, so production consumers expand this turn back to the
/// covered original cells. The default preserves the FW1 vocabulary and
/// every diagnostic sink that only needs the coarse turn.</summary>
void OnLandscapeCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
OnLandscapeCellTurn(
(landblockId & 0xFFFF0000u) | checked((uint)(cellIndex + 1)));
/// <summary>
/// Fires at <see cref="RetailFrameWalk.DrawBuilding"/> once retail's own
/// gate has passed — <c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0

View file

@ -223,6 +223,13 @@ internal enum WalkFrameEventKind : byte
/// was recorded.</summary>
StreamMark,
/// <summary>Submits every retained translucent walk batch through
/// <see cref="WbDrawDispatcher.SubmitWalkAlphaInstance"/> up to the
/// exclusive-end index in <see cref="WalkFrameEvent.IntArg"/>. This is a
/// replay-time event: collecting the complete frame must not make alpha
/// from a later/near cell visible to an earlier building barrier.</summary>
AlphaSubmitMark,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawSky"/>.</summary>
Sky,
@ -351,6 +358,9 @@ internal readonly struct WalkFrameEvent
internal static WalkFrameEvent Mark(int exclusiveEnd) =>
new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null);
internal static WalkFrameEvent AlphaSubmitMark(int exclusiveEnd) =>
new(WalkFrameEventKind.AlphaSubmitMark, exclusiveEnd, 0, 0f, null);
internal static WalkFrameEvent Sky() =>
new(WalkFrameEventKind.Sky, 0, 0, 0f, null);
@ -472,6 +482,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
private readonly OrderedDrawStream _stream = new();
private readonly List<WalkFrameEvent> _events = new();
private readonly List<int> _markPositions = new();
private readonly List<WbDrawDispatcher.WalkClassifiedBatch> _alphaSubmissions = new();
private int _alphaSubmitMark;
// Campaign FW3.4a: visited-set collection, absorbed from the renderer's
// former dedicated set-collecting sink (RetailPViewRenderer's old
@ -603,6 +615,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_alphaSubmitMark = 0;
VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
@ -736,6 +750,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_alphaSubmitMark = 0;
VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
@ -764,6 +780,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
try
{
MarkIfGrown();
MarkAlphaIfGrown();
}
finally
{
@ -804,6 +821,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
int cursor = 0;
int alphaCursor = 0;
for (int i = 0; i < _events.Count; i++)
{
WalkFrameEvent e = _events[i];
@ -817,6 +835,18 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_dispatcher.DrawOrderedRange(encoder, cursor, count);
cursor = end;
break;
case WalkFrameEventKind.AlphaSubmitMark:
int alphaEnd = e.IntArg;
for (; alphaCursor < alphaEnd; alphaCursor++)
{
WbDrawDispatcher.WalkClassifiedBatch batch =
_alphaSubmissions[alphaCursor];
_dispatcher.SubmitWalkAlphaInstance(
in batch,
_cameraWorldPosition,
_viewProjection);
}
break;
case WalkFrameEventKind.Sky:
_leafRenderer.DrawSky();
break;
@ -883,6 +913,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_alphaSubmitMark = 0;
_readyToReplay = false;
_ctx = null;
}
@ -916,6 +948,50 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
}
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
=> HandleLandscapeCellTurn(cellId);
void IWalkEventSink.OnLandscapeCellTurn(
uint landblockId,
int sideCellCount,
int cellIndex)
{
if (sideCellCount is not (1 or 2 or 4 or 8))
{
throw new ArgumentOutOfRangeException(
nameof(sideCellCount),
sideCellCount,
"A landscape LOD grid must be 1, 2, 4, or 8 cells per side.");
}
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
throw new ArgumentOutOfRangeException(nameof(cellIndex));
uint blockPrefix = landblockId & 0xFFFF0000u;
if (sideCellCount == 8)
{
HandleLandscapeCellTurn(blockPrefix | checked((uint)(cellIndex + 1)));
return;
}
// CLandBlock's coarse DrawSortCell owns the shadows from every
// original 24 m land cell it covers. Our render journal deliberately
// retains those authoritative 8x8 owner buckets, so turn all covered
// buckets here while preserving the coarse cell's single walk turn.
int span = 8 / sideCellCount;
int coarseX = cellIndex / sideCellCount;
int coarseY = cellIndex % sideCellCount;
int firstX = coarseX * span;
int firstY = coarseY * span;
for (int x = firstX; x < firstX + span; x++)
{
for (int y = firstY; y < firstY + span; y++)
{
HandleLandscapeCellTurn(
blockPrefix | checked((uint)(x * 8 + y + 1)));
}
}
}
private void HandleLandscapeCellTurn(uint cellId)
{
RequireOpenFrame();
if (_landscapeViewRouteIndex < 0)
@ -930,11 +1006,18 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_populator.PopulateOutdoorStatics(
_stream, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame);
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame,
_alphaSubmissions);
_populator.PopulateCellDynamics(
_stream, cellId, dynamics.Records, dynamics.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame);
this, _landscapeViewRouteIndex, _outdoorDrawnThisFrame,
_alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
{
MarkIfGrown();
MarkAlphaIfGrown();
}
// 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
@ -977,6 +1060,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
// look-ins) follows this call; the building's own shell content is
// appended only once that pass completes (OnBuildingShellTurn).
MarkIfGrown();
MarkAlphaIfGrown();
_events.Add(WalkFrameEvent.AlphaBarrier());
_currentDcStage = WalkDrawStage.LookInStatic;
@ -996,7 +1080,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
_populator.PopulateCell(
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection);
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
alphaSubmissions: _alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
{
MarkIfGrown();
MarkAlphaIfGrown();
}
// FW4 (the #132 positional invariant): the building's own shell
// emitters submit at the shell turn, after the shell content
// flushes — see WalkFrameEventKind.StaticParticles.
@ -1166,8 +1256,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_populator.PopulateCell(
_stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, viewRouteIndex);
this, viewRouteIndex, _alphaSubmissions);
MarkIfGrown();
MarkAlphaIfGrown();
if (stage == WalkDrawStage.LookInStatic)
{
@ -1184,8 +1275,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_cameraWorldPosition,
_viewProjection,
this,
viewRouteIndex);
viewRouteIndex,
alphaSubmissions: _alphaSubmissions);
MarkIfGrown();
MarkAlphaIfGrown();
if (HasAnyOwner(records) || HasAnyOwner(dynamics))
_events.Add(WalkFrameEvent.CellParticles(cellId));
}
@ -1364,6 +1457,20 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Add(WalkFrameEvent.Mark(count));
}
/// <summary>Records translucent walk batches at the same temporal point
/// as their cell turn. Replay submits them only when it reaches this
/// marker, so an earlier building's full alpha barrier cannot see content
/// that the far-to-near walk has not reached yet.</summary>
private void MarkAlphaIfGrown()
{
int count = _alphaSubmissions.Count;
if (count == _alphaSubmitMark)
return;
_alphaSubmitMark = count;
_events.Add(WalkFrameEvent.AlphaSubmitMark(count));
}
private IWalkBuildingFrameContext RequireOpenFrame() =>
_ctx ?? throw new InvalidOperationException(
"WalkFrameDriver received a walk turn outside BeginFrame/EndFrame — call "

View file

@ -66,7 +66,8 @@ internal sealed class WalkStaticStreamPopulator
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? views = null,
int viewRouteIndex = -1)
int viewRouteIndex = -1,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
@ -74,7 +75,7 @@ internal sealed class WalkStaticStreamPopulator
ClassifyAndAppend(
stream, stage, cellId, in records[i], tupleLandblockId,
cameraWorldPosition, viewProjection,
liveDynamic: false, views, viewRouteIndex);
liveDynamic: false, views, viewRouteIndex, alphaSubmissions);
}
}
@ -98,7 +99,8 @@ internal sealed class WalkStaticStreamPopulator
Matrix4x4 viewProjection,
IWalkLookInViewSource? views = null,
int viewRouteIndex = -1,
ISet<RenderProjectionId>? drawnOnce = null)
ISet<RenderProjectionId>? drawnOnce = null,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
@ -108,7 +110,7 @@ internal sealed class WalkStaticStreamPopulator
ClassifyAndAppend(
stream, WalkDrawStage.OutdoorStatic, cellId, in records[i],
tupleLandblockId, cameraWorldPosition, viewProjection,
liveDynamic: false, views, viewRouteIndex);
liveDynamic: false, views, viewRouteIndex, alphaSubmissions);
}
}
@ -121,7 +123,8 @@ internal sealed class WalkStaticStreamPopulator
Matrix4x4 viewProjection,
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1,
ISet<RenderProjectionId>? drawnOnce = null)
ISet<RenderProjectionId>? drawnOnce = null,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
@ -138,7 +141,8 @@ internal sealed class WalkStaticStreamPopulator
viewProjection,
liveDynamic: true,
lookInViews,
lookInRouteIndex);
lookInRouteIndex,
alphaSubmissions);
}
}
@ -152,7 +156,8 @@ internal sealed class WalkStaticStreamPopulator
Matrix4x4 viewProjection,
bool liveDynamic = false,
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1)
int lookInRouteIndex = -1,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
_batchScratch.Clear();
_selectionScratch.Clear();
@ -178,8 +183,15 @@ internal sealed class WalkStaticStreamPopulator
}
else
{
_dispatcher.SubmitWalkAlphaInstance(
in batch, cameraWorldPosition, viewProjection);
if (alphaSubmissions is null)
{
_dispatcher.SubmitWalkAlphaInstance(
in batch, cameraWorldPosition, viewProjection);
}
else
{
alphaSubmissions.Add(batch);
}
}
}