perf(rendering): retain synchronous frame scratch

Reuse the PView frame input, publish mutation-invalidated landblock views, and avoid constructing optional shadow iterators while preserving title and lifecycle visibility facts.
This commit is contained in:
Erik 2026-07-25 05:28:30 +02:00
parent b9cbf5e040
commit b3427554c3
10 changed files with 397 additions and 129 deletions

View file

@ -418,34 +418,32 @@ public sealed class RetailPViewPassExecutorTests
null));
}
return new RetailPViewFrameInput
{
RootCell = root,
NearbyBuildingCells = nearbyBuildingCells,
ViewerEyePos = Vector3.Zero,
ViewProjection = TestCamera.ViewProjection,
Cells = new DictionaryCellSource(cells),
Camera = new TestCamera(),
CameraWorldPosition = Vector3.Zero,
Frustum = null,
PlayerLandblockId = root.CellId & 0xFFFF0000u,
AnimatedEntityIds = null,
RenderCenterLbX = 0,
RenderCenterLbY = 0,
RenderRadius = 1,
LandblockEntries = entries,
RenderSky = true,
RenderWeather = true,
DayFraction = 0f,
ActiveDayGroup = null,
SkyKeyframe = default,
EnvironOverrideActive = false,
ViewerCellId = root.CellId,
PlayerCellId = root.CellId,
PlayerViewPosition = Vector3.Zero,
CameraView = TestCamera.ViewMatrix,
CameraCellResolution = CameraCellResolution.None,
};
return new RetailPViewFrameInput().Reset(
root,
nearbyBuildingCells,
Vector3.Zero,
TestCamera.ViewProjection,
new DictionaryCellSource(cells),
new TestCamera(),
Vector3.Zero,
frustum: null,
playerLandblockId: root.CellId & 0xFFFF0000u,
animatedEntityIds: null,
renderCenterLbX: 0,
renderCenterLbY: 0,
renderRadius: 1,
landblockEntries: entries,
renderSky: true,
renderWeather: true,
dayFraction: 0f,
activeDayGroup: null,
skyKeyframe: default,
environOverrideActive: false,
viewerCellId: root.CellId,
playerCellId: root.CellId,
playerViewPosition: Vector3.Zero,
cameraView: TestCamera.ViewMatrix,
cameraCellResolution: CameraCellResolution.None);
}
private static LoadedCell InteriorWithExit(uint cellId)

View file

@ -153,6 +153,32 @@ public sealed class WorldSceneRendererTests
Assert.Equal(4, rig.PView.LastInput.RenderRadius);
}
[Fact]
public void PViewWorld_ReusesOneSynchronousFrameInputAcrossFrames()
{
var root = new LoadedCell
{
CellId = 0x01010001u,
IsOutdoorNode = false,
};
var rig = new Rig(
portalVisible: false,
waitingForLogin: false,
clipRoot: root);
rig.Renderer.Render(default);
RetailPViewFrameInput first = Assert.IsType<RetailPViewFrameInput>(
rig.PView.LastInput);
rig.Calls.Clear();
rig.Renderer.Render(default);
Assert.Same(first, rig.PView.LastInput);
Assert.Same(root, rig.PView.LastInput!.RootCell);
Assert.Equal(rig.DayFraction, rig.PView.LastInput.DayFraction);
Assert.Contains("pview:draw", rig.Calls);
}
[Fact]
public void OutdoorPView_UsesPViewOwnersForPostWorldParticlesWithoutFlatWeather()
{
@ -474,13 +500,13 @@ public sealed class WorldSceneRendererTests
private sealed class EntitySource : IWorldSceneEntitySource
{
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
Array.Empty<(uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
IReadOnlyDictionary<uint, WorldEntity>?)>();
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
Array.Empty<(uint, Vector3, Vector3)>();
}
@ -697,7 +723,7 @@ public sealed class WorldSceneRendererTests
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
{
calls.Add("diagnostics:draw");
return new WorldSceneDiagnosticOutcome(3, 8);

View file

@ -68,6 +68,44 @@ public sealed class GpuWorldStateRenderTraversalTests
Assert.False(state.TryGetRenderTraversalSortKey(pending, out _));
}
[Fact]
public void StableRenderViews_AllocateOnlyWhenSpatialContentChanges()
{
var state = new GpuWorldState();
state.AddLandblock(Landblock(
FirstLandblock,
Entity(1, 0x80000001u)));
state.SetLandblockAabb(
FirstLandblock,
new Vector3(1f, 2f, 3f),
new Vector3(4f, 5f, 6f));
var entries = state.LandblockEntries;
var bounds = state.LandblockBounds;
_ = entries.Count;
_ = bounds.Count;
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 1_000; iteration++)
{
_ = state.LandblockEntries.Count;
_ = state.LandblockBounds.Count;
}
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
Assert.Same(entries, state.LandblockEntries);
Assert.Same(bounds, state.LandblockBounds);
state.AddLandblock(Landblock(
SecondLandblock,
Entity(2, 0x80000002u)));
Assert.Same(entries, state.LandblockEntries);
Assert.Same(bounds, state.LandblockBounds);
Assert.Equal(2, entries.Count);
Assert.Equal(2, bounds.Count);
}
private static void AssertSort(
GpuWorldState state,
WorldEntity entity,