refactor(render): move outdoor dynamics onto landscape turns
This commit is contained in:
parent
474b05e7dc
commit
2127c955f6
8 changed files with 151 additions and 12 deletions
|
|
@ -1211,8 +1211,17 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
// statics, live dynamics, fades, picking, and particles. Do not
|
||||
// advertise a packed LookInObject range the dispatcher cannot and
|
||||
// must not consume a second time.
|
||||
BuildOutsideDynamicRoutes(writer, in input);
|
||||
BuildDynamicLastRoute(writer, in input);
|
||||
// FW4 cutover: once a walk view source is present, every live
|
||||
// object is emitted at its retail cell turn (landscape, root
|
||||
// flood, or building look-in). The packed dynamic ranges exist
|
||||
// only for non-walk diagnostic callers during this atomic slice;
|
||||
// advertising either range in production would draw the same
|
||||
// object a second time after its owner cell.
|
||||
if (input.WalkLookInViews is null)
|
||||
{
|
||||
BuildOutsideDynamicRoutes(writer, in input);
|
||||
BuildDynamicLastRoute(writer, in input);
|
||||
}
|
||||
writer.Publish();
|
||||
AcknowledgeCachedDirtyRecords();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ public sealed class RetailFrameWalk
|
|||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||
{
|
||||
sink.Emit(WalkEvent.Landscape(activeViews.ViewCount));
|
||||
sink.OnLandscapeViews(activeViews);
|
||||
landscape.CalcDrawOrder();
|
||||
landscape.CheckBlocks(ctx.CyPlane, activeViews);
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,14 @@ public interface IWalkEventSink
|
|||
{
|
||||
void Emit(in WalkEvent walkEvent);
|
||||
|
||||
/// <summary>
|
||||
/// Installs the exact view set <c>LScape::draw</c> is walking. The same
|
||||
/// view set gates every landscape object's drawing sphere at its cell
|
||||
/// turn (<c>Render::viewconeCheck</c>); consumers must copy any data they
|
||||
/// retain because the walk owns and reuses this object.
|
||||
/// </summary>
|
||||
void OnLandscapeViews(WalkPortalView activeViews) { }
|
||||
|
||||
/// <summary>
|
||||
/// Fires once per visited landscape cell, AFTER that cell's building
|
||||
/// turn (if any) — <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ internal interface IWalkFrameWorldData
|
|||
/// <see cref="IWalkEventSink.OnLandscapeCellTurn"/> computes.</summary>
|
||||
WalkFrameStaticRecords GetOutdoorStatics(uint cellId);
|
||||
|
||||
/// <summary>One visited landscape cell's live dynamic occupants. The
|
||||
/// walk draws these at the same <c>DrawObjCell</c> turn as its statics,
|
||||
/// through the landscape's currently installed view set.</summary>
|
||||
WalkFrameStaticRecords GetOutdoorDynamics(uint cellId);
|
||||
|
||||
/// <summary>One building's own exterior shell content (<c>IsBuildingShell</c>
|
||||
/// records anchored at the building's position cell).</summary>
|
||||
WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building);
|
||||
|
|
@ -519,6 +524,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private WalkDrawStage? _currentDcStage;
|
||||
private bool _readyToReplay;
|
||||
private int _cellViewRouteIndex;
|
||||
private int _landscapeViewRouteIndex;
|
||||
|
||||
internal WalkFrameDriver(
|
||||
WbDrawDispatcher dispatcher,
|
||||
|
|
@ -627,6 +633,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
VisitedLandscapeCellIds.Clear();
|
||||
InteriorFloodCells.Clear();
|
||||
_cellViewRouteIndex = 0;
|
||||
_landscapeViewRouteIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame
|
||||
|
|
@ -719,6 +726,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
? _worldData.GetBuildingShellStatics(shellOwner)
|
||||
: _worldData.GetOutdoorStatics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
if (e.Building is null)
|
||||
{
|
||||
UnionOwners(
|
||||
_worldData.GetOutdoorDynamics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
}
|
||||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||
break;
|
||||
|
|
@ -776,24 +789,44 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
|
||||
{
|
||||
RequireOpenFrame();
|
||||
if (_landscapeViewRouteIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A landscape cell turn fired before RetailFrameWalk installed its active "
|
||||
+ "view set — the walk and draw driver are desynchronized.");
|
||||
}
|
||||
VisitedLandscapeCellIds.Add(cellId);
|
||||
WalkFrameStaticRecords records = _worldData.GetOutdoorStatics(cellId);
|
||||
WalkFrameStaticRecords dynamics = _worldData.GetOutdoorDynamics(cellId);
|
||||
_populator.PopulateOutdoorStatics(
|
||||
_stream, cellId, records.Records, records.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection);
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, _landscapeViewRouteIndex);
|
||||
_populator.PopulateCellDynamics(
|
||||
_stream, cellId, dynamics.Records, dynamics.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection,
|
||||
this, _landscapeViewRouteIndex);
|
||||
// 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
|
||||
// WalkFrameEventKind.StaticParticles. Mark first so the cell's own
|
||||
// meshes flush ahead of its particle submission (retail's
|
||||
// per-object DrawObjCell order).
|
||||
if (HasAnyOwner(records))
|
||||
if (HasAnyOwner(records) || HasAnyOwner(dynamics))
|
||||
{
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
||||
}
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnLandscapeViews(WalkPortalView activeViews)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(activeViews);
|
||||
RequireOpenFrame();
|
||||
_landscapeViewRouteIndex = _cellViewRouteIndex++;
|
||||
CaptureViews(0, activeViews);
|
||||
}
|
||||
|
||||
private static bool HasAnyOwner(in WalkFrameStaticRecords records)
|
||||
{
|
||||
foreach (RenderProjectionRecord record in records.Records)
|
||||
|
|
@ -1020,7 +1053,11 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
return;
|
||||
}
|
||||
|
||||
WalkPortalView portalView = cell.TopView;
|
||||
CaptureViews(cellId, cell.TopView);
|
||||
}
|
||||
|
||||
private void CaptureViews(uint cellId, WalkPortalView portalView)
|
||||
{
|
||||
int sliceStart = _lookInSlices.Count;
|
||||
for (int sliceIndex = 0; sliceIndex < portalView.ViewCount; sliceIndex++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,10 +63,13 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellDynamicCache = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorByCell = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorDynamicsByCell = new();
|
||||
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorDynamicsMaterialized = new();
|
||||
private readonly Dictionary<uint, WalkFrameStaticRecords> _shellMaterialized = new();
|
||||
private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
|
||||
private RenderProjectionRecord[] _dynamicSweepScratch = new RenderProjectionRecord[256];
|
||||
private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256];
|
||||
private RenderProjectionRecord[] _cellDynamicScratch = new RenderProjectionRecord[256];
|
||||
|
||||
|
|
@ -102,10 +105,13 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
_cellCache.Clear();
|
||||
_cellDynamicCache.Clear();
|
||||
_outdoorMaterialized.Clear();
|
||||
_outdoorDynamicsMaterialized.Clear();
|
||||
_shellMaterialized.Clear();
|
||||
_arenaLength = 0;
|
||||
foreach (List<RenderProjectionRecord> bucket in _outdoorByCell.Values)
|
||||
bucket.Clear();
|
||||
foreach (List<RenderProjectionRecord> bucket in _outdoorDynamicsByCell.Values)
|
||||
bucket.Clear();
|
||||
foreach (List<RenderProjectionRecord> bucket in _shellsByAnchor.Values)
|
||||
bucket.Clear();
|
||||
|
||||
|
|
@ -137,6 +143,29 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
_outdoorByCell[cellId] = bucket = new List<RenderProjectionRecord>();
|
||||
bucket.Add(record);
|
||||
}
|
||||
|
||||
required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorDynamic);
|
||||
if (required > _dynamicSweepScratch.Length)
|
||||
{
|
||||
_dynamicSweepScratch = new RenderProjectionRecord[
|
||||
Math.Max(required, _dynamicSweepScratch.Length * 2)];
|
||||
}
|
||||
count = _scene.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorDynamic,
|
||||
_dynamicSweepScratch);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The landscape cell owning a RENDER-ORIGIN-RELATIVE position
|
||||
|
|
@ -216,6 +245,27 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId)
|
||||
{
|
||||
if (_outdoorDynamicsMaterialized.TryGetValue(
|
||||
cellId,
|
||||
out WalkFrameStaticRecords cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
WalkFrameStaticRecords records =
|
||||
_outdoorDynamicsByCell.TryGetValue(
|
||||
cellId,
|
||||
out List<RenderProjectionRecord>? bucket)
|
||||
&& bucket.Count > 0
|
||||
? new WalkFrameStaticRecords(
|
||||
AppendToArena(CollectionsMarshal.AsSpan(bucket)), _tupleLandblockId)
|
||||
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
|
||||
_outdoorDynamicsMaterialized[cellId] = records;
|
||||
return records;
|
||||
}
|
||||
|
||||
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
|
||||
{
|
||||
uint anchor = AnchorCellId(building);
|
||||
|
|
|
|||
|
|
@ -92,10 +92,19 @@ internal sealed class WalkStaticStreamPopulator
|
|||
ReadOnlySpan<RenderProjectionRecord> records,
|
||||
uint tupleLandblockId,
|
||||
Vector3 cameraWorldPosition,
|
||||
Matrix4x4 viewProjection) =>
|
||||
PopulateCell(
|
||||
stream, WalkDrawStage.OutdoorStatic, cellId, records,
|
||||
tupleLandblockId, cameraWorldPosition, viewProjection);
|
||||
Matrix4x4 viewProjection,
|
||||
IWalkLookInViewSource? views = null,
|
||||
int viewRouteIndex = -1)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
for (int i = 0; i < records.Length; i++)
|
||||
{
|
||||
ClassifyAndAppend(
|
||||
stream, WalkDrawStage.OutdoorStatic, cellId, in records[i],
|
||||
tupleLandblockId, cameraWorldPosition, viewProjection,
|
||||
liveDynamic: false, views, viewRouteIndex);
|
||||
}
|
||||
}
|
||||
|
||||
internal void PopulateCellDynamics(
|
||||
OrderedDrawStream stream,
|
||||
|
|
|
|||
|
|
@ -273,10 +273,14 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
0x0100_0000_0000_0023,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord outdoorDynamic = Record(
|
||||
0x0100_0000_0000_0024,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, lookInDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 2, rootDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 3, outdoorDynamic),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
|
|
@ -309,6 +313,9 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
Assert.DoesNotContain(
|
||||
frame.RouteCandidates.ToArray(),
|
||||
candidate => candidate.Id == rootDynamic.Id);
|
||||
Assert.DoesNotContain(
|
||||
frame.RouteCandidates.ToArray(),
|
||||
candidate => candidate.Id == outdoorDynamic.Id);
|
||||
Assert.NotNull(frame.WalkLookInViews);
|
||||
Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn(
|
||||
routeIndex: 0,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ public sealed class WalkFrameDriverTests
|
|||
public readonly Dictionary<uint, WalkFrameStaticRecords> CellStaticsByCell = new();
|
||||
public readonly Dictionary<uint, WalkFrameStaticRecords> CellDynamicsByCell = new();
|
||||
public readonly Dictionary<uint, WalkFrameStaticRecords> OutdoorStaticsByCell = new();
|
||||
public readonly Dictionary<uint, WalkFrameStaticRecords> OutdoorDynamicsByCell = new();
|
||||
public readonly Dictionary<WalkBuilding, WalkFrameStaticRecords> ShellByBuilding = new();
|
||||
public readonly Dictionary<WalkBuilding, Matrix4x4> WorldTransformByBuilding = new();
|
||||
|
||||
|
|
@ -104,6 +105,9 @@ public sealed class WalkFrameDriverTests
|
|||
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) =>
|
||||
OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
|
||||
|
||||
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) =>
|
||||
OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
|
||||
|
||||
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) =>
|
||||
ShellByBuilding.GetValueOrDefault(building, WalkFrameStaticRecords.Empty);
|
||||
|
||||
|
|
@ -601,12 +605,22 @@ public sealed class WalkFrameDriverTests
|
|||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[0x8C040005u] = new WalkFrameStaticRecords(
|
||||
new[] { MakeRecord(301, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
worldData.OutdoorDynamicsByCell[0x8C040005u] = new WalkFrameStaticRecords(
|
||||
new[] { MakeRecord(302, 0x50000001, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher, new RecordingLeafRenderer(log), worldData, new RecordingTrace(log));
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
var activeViews = new WalkPortalView();
|
||||
WalkCopyView.AppendFullViewportQuad(
|
||||
activeViews,
|
||||
ctx.Rays,
|
||||
ctx.WorldViewpoint,
|
||||
ctx.ViewportWidth,
|
||||
ctx.ViewportHeight);
|
||||
((IWalkEventSink)driver).OnLandscapeViews(activeViews);
|
||||
((IWalkEventSink)driver).OnLandscapeCellTurn(0x8C040005u);
|
||||
Assert.Empty(log); // no GPU work at Collect time; nothing recorded to the log yet
|
||||
driver.EndFrame();
|
||||
|
|
@ -615,9 +629,13 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
// FW4 #132 positional invariant: the cell's emitters submit at its
|
||||
// own landscape turn, after its meshes flush.
|
||||
Assert.Equal(new[] { "FLUSH:1:OutdoorStatic", "PARTICLES:12d" }, log);
|
||||
GpuRecordedMultiDrawIndirect mdi = Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
Assert.Equal(1u, mdi.DrawCount);
|
||||
Assert.Equal(
|
||||
new[] { "FLUSH:2:OutdoorStatic,Dynamic", "PARTICLES:12d,12e" },
|
||||
log);
|
||||
GpuRecordedMultiDrawIndirect[] mdi =
|
||||
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().ToArray();
|
||||
Assert.Equal(2, mdi.Length);
|
||||
Assert.Equal(2u, mdi.Aggregate(0u, static (sum, call) => sum + call.DrawCount));
|
||||
}
|
||||
|
||||
// ── Fixture (mirrors WalkStaticStreamPopulatorTests' DispatcherFixture —
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue