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

@ -1321,41 +1321,100 @@ internal sealed class BuildingGroupScratch
public sealed class RetailPViewFrameInput
{
public required LoadedCell RootCell { get; init; }
public LoadedCell RootCell { get; private set; } = null!;
/// <summary>R-A2: nearby building cells (BuildingId-tagged) flooded per-building when the root is the
/// outdoor node. Null for interior roots. Grouped by BuildingId inside <see cref="DrawInside"/>.</summary>
public IReadOnlyList<LoadedCell>? NearbyBuildingCells { get; init; }
public IReadOnlyList<LoadedCell>? NearbyBuildingCells { get; private set; }
public required Vector3 ViewerEyePos { get; init; }
public required Matrix4x4 ViewProjection { get; init; }
public required IRetailPViewCellSource Cells { get; init; }
public required ICamera Camera { get; init; }
public required Vector3 CameraWorldPosition { get; init; }
public required FrustumPlanes? Frustum { get; init; }
public required uint? PlayerLandblockId { get; init; }
public required HashSet<uint>? AnimatedEntityIds { get; init; }
public required int RenderCenterLbX { get; init; }
public required int RenderCenterLbY { get; init; }
public required int RenderRadius { get; init; }
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
public Vector3 ViewerEyePos { get; private set; }
public Matrix4x4 ViewProjection { get; private set; }
public IRetailPViewCellSource Cells { get; private set; } = null!;
public ICamera Camera { get; private set; } = null!;
public Vector3 CameraWorldPosition { get; private set; }
public FrustumPlanes? Frustum { get; private set; }
public uint? PlayerLandblockId { get; private set; }
public HashSet<uint>? AnimatedEntityIds { get; private set; }
public int RenderCenterLbX { get; private set; }
public int RenderCenterLbY { get; private set; }
public int RenderRadius { get; private set; }
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
{ get; init; }
{ get; private set; } = Array.Empty<(uint, Vector3, Vector3,
IReadOnlyList<WorldEntity>,
IReadOnlyDictionary<uint, WorldEntity>?)>();
// Pass-presentation and diagnostic values consumed synchronously by the
// typed executor. This input is data-only and is never retained.
public required bool RenderSky { get; init; }
public required bool RenderWeather { get; init; }
public required float DayFraction { get; init; }
public required DayGroupData? ActiveDayGroup { get; init; }
public required SkyKeyframe SkyKeyframe { get; init; }
public required bool EnvironOverrideActive { get; init; }
public required uint ViewerCellId { get; init; }
public required uint PlayerCellId { get; init; }
public required Vector3 PlayerViewPosition { get; init; }
public required Matrix4x4 CameraView { get; init; }
public required CameraCellResolution CameraCellResolution { get; init; }
public bool RenderSky { get; private set; }
public bool RenderWeather { get; private set; }
public float DayFraction { get; private set; }
public DayGroupData? ActiveDayGroup { get; private set; }
public SkyKeyframe SkyKeyframe { get; private set; }
public bool EnvironOverrideActive { get; private set; }
public uint ViewerCellId { get; private set; }
public uint PlayerCellId { get; private set; }
public Vector3 PlayerViewPosition { get; private set; }
public Matrix4x4 CameraView { get; private set; }
public CameraCellResolution CameraCellResolution { get; private set; }
internal RetailPViewFrameInput Reset(
LoadedCell rootCell,
IReadOnlyList<LoadedCell>? nearbyBuildingCells,
Vector3 viewerEyePos,
Matrix4x4 viewProjection,
IRetailPViewCellSource cells,
ICamera camera,
Vector3 cameraWorldPosition,
FrustumPlanes? frustum,
uint? playerLandblockId,
HashSet<uint>? animatedEntityIds,
int renderCenterLbX,
int renderCenterLbY,
int renderRadius,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
bool renderSky,
bool renderWeather,
float dayFraction,
DayGroupData? activeDayGroup,
SkyKeyframe skyKeyframe,
bool environOverrideActive,
uint viewerCellId,
uint playerCellId,
Vector3 playerViewPosition,
Matrix4x4 cameraView,
CameraCellResolution cameraCellResolution)
{
RootCell = rootCell;
NearbyBuildingCells = nearbyBuildingCells;
ViewerEyePos = viewerEyePos;
ViewProjection = viewProjection;
Cells = cells;
Camera = camera;
CameraWorldPosition = cameraWorldPosition;
Frustum = frustum;
PlayerLandblockId = playerLandblockId;
AnimatedEntityIds = animatedEntityIds;
RenderCenterLbX = renderCenterLbX;
RenderCenterLbY = renderCenterLbY;
RenderRadius = renderRadius;
LandblockEntries = landblockEntries;
RenderSky = renderSky;
RenderWeather = renderWeather;
DayFraction = dayFraction;
ActiveDayGroup = activeDayGroup;
SkyKeyframe = skyKeyframe;
EnvironOverrideActive = environOverrideActive;
ViewerCellId = viewerCellId;
PlayerCellId = playerCellId;
PlayerViewPosition = playerViewPosition;
CameraView = cameraView;
CameraCellResolution = cameraCellResolution;
return this;
}
}
/// <summary>

View file

@ -44,7 +44,7 @@ internal interface IWorldSceneDiagnostics
WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
}
/// <summary>
@ -168,16 +168,16 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
{
ArgumentNullException.ThrowIfNull(bounds);
DrawCollisionWireframes(in camera);
int visible = 0;
int total = 0;
foreach (var entry in bounds)
int total = bounds.Count;
for (int index = 0; index < bounds.Count; index++)
{
total++;
var entry = bounds[index];
if (FrustumCuller.IsAabbVisible(
camera.Frustum,
entry.AabbMin,
@ -187,16 +187,21 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
}
}
Vector3 nearestOrigin =
_mode.IsPlayerMode && _player.Controller is { } controller
? controller.Position
: camera.Position;
_debugVm.PublishDebugVmFacts(
_debugVmConsumerActive,
visible,
total,
nearestOrigin,
_physics.ShadowObjects.AllEntriesForDebug());
// AllEntriesForDebug is a yield sequence. Do not even construct its
// iterator unless the developer UI is mounted and consumes the facts.
if (_debugVmConsumerActive)
{
Vector3 nearestOrigin =
_mode.IsPlayerMode && _player.Controller is { } controller
? controller.Position
: camera.Position;
_debugVm.PublishDebugVmFacts(
consumerActive: true,
visible,
total,
nearestOrigin,
_physics.ShadowObjects.AllEntriesForDebug());
}
return new WorldSceneDiagnosticOutcome(visible, total);
}

View file

@ -61,6 +61,7 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
private readonly IWorldRenderRangeSource _renderRange;
private readonly IWorldSceneDiagnostics _diagnostics;
private readonly IWorldGenerationAvailability _availability;
private readonly RetailPViewFrameInput _pviewFrameInput = new();
public WorldSceneRenderer(
IRenderFrameFoundationSource foundation,
@ -174,34 +175,32 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
{
pviewFrameStarted = true;
pviewResult = _pview.DrawInside(
new RetailPViewFrameInput
{
RootCell = clipRoot,
NearbyBuildingCells = world.Buildings.NearbyBuildingCells,
ViewerEyePos = roots.ViewerEyePosition,
ViewProjection = camera.ViewProjection,
Cells = _pviewCells,
Camera = camera.Camera,
CameraWorldPosition = camera.Position,
Frustum = camera.Frustum,
PlayerLandblockId = roots.PlayerLandblockId,
AnimatedEntityIds = world.AnimatedEntityIds,
RenderCenterLbX = roots.RenderCenterLandblockX,
RenderCenterLbY = roots.RenderCenterLandblockY,
RenderRadius = _renderRange.NearRadius,
LandblockEntries = _entities.LandblockEntries,
RenderSky = renderSky,
RenderWeather = roots.PlayerSeenOutside,
DayFraction = _sky.DayFraction,
ActiveDayGroup = _sky.ActiveDayGroup,
SkyKeyframe = foundation.Sky,
EnvironOverrideActive = foundation.EnvironOverrideActive,
ViewerCellId = roots.ViewerCellId,
PlayerCellId = roots.PlayerCellId,
PlayerViewPosition = roots.PlayerViewPosition,
CameraView = camera.Camera.View,
CameraCellResolution = _diagnostics.CameraCellResolution,
});
_pviewFrameInput.Reset(
clipRoot,
world.Buildings.NearbyBuildingCells,
roots.ViewerEyePosition,
camera.ViewProjection,
_pviewCells,
camera.Camera,
camera.Position,
camera.Frustum,
roots.PlayerLandblockId,
world.AnimatedEntityIds,
roots.RenderCenterLandblockX,
roots.RenderCenterLandblockY,
_renderRange.NearRadius,
_entities.LandblockEntries,
renderSky,
roots.PlayerSeenOutside,
_sky.DayFraction,
_sky.ActiveDayGroup,
foundation.Sky,
foundation.EnvironOverrideActive,
roots.ViewerCellId,
roots.PlayerCellId,
roots.PlayerViewPosition,
camera.Camera.View,
_diagnostics.CameraCellResolution));
_particleVisibility.MarkVisibleCells(pviewResult.DrawableCells);
_frames.ObserveDrawableCells(pviewResult.DrawableCells);

View file

@ -15,12 +15,12 @@ internal interface IWorldSceneSkyStateSource
internal interface IWorldSceneEntitySource
{
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
{ get; }
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds { get; }
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds { get; }
}
internal sealed class RuntimeWorldSceneEntitySource : IWorldSceneEntitySource
@ -32,12 +32,12 @@ internal sealed class RuntimeWorldSceneEntitySource : IWorldSceneEntitySource
_world = world ?? throw new ArgumentNullException(nameof(world));
}
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
_world.LandblockEntries;
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
_world.LandblockBounds;
}