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:
parent
b9cbf5e040
commit
b3427554c3
10 changed files with 397 additions and 129 deletions
|
|
@ -80,21 +80,26 @@ subtree-withdrawal suites preserve exact event and resource counts.
|
||||||
Landed evidence:
|
Landed evidence:
|
||||||
[`../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md`](../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md).
|
[`../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md`](../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md).
|
||||||
|
|
||||||
### H-a4 — diagnostics and frame scratch
|
### H-a4 — diagnostics and frame scratch — COMPLETE
|
||||||
|
|
||||||
1. Reuse one synchronous `RetailPViewFrameInput` owned by
|
1. Reuse one synchronous `RetailPViewFrameInput` owned by
|
||||||
`WorldSceneRenderer`; reset every field before each draw.
|
`WorldSceneRenderer`; reset every field before each draw.
|
||||||
2. Expose stable borrowed landblock entry/bounds views instead of creating
|
2. Expose stable borrowed landblock entry/bounds views instead of creating
|
||||||
iterator state in each frame.
|
iterator state in each frame.
|
||||||
3. Consumer-gate diagnostic landblock visibility counting and shadow
|
3. Consumer-gate optional shadow enumeration. Preserve landblock visibility
|
||||||
enumeration. When no DebugVM/collision/probe consumer exists, publish a
|
counting because the normal title and lifecycle checkpoints are active
|
||||||
neutral diagnostic outcome without scanning resident landblocks.
|
consumers of `lb V/T`; neutralizing it would be a visible/automation
|
||||||
|
behavior change.
|
||||||
4. Do not rework `InteriorEntityPartition` while the G4 referee remains.
|
4. Do not rework `InteriorEntityPartition` while the G4 referee remains.
|
||||||
Issue #241 closes with G5 old-route deletion, not by editing the fallback.
|
Issue #241 closes with G5 old-route deletion, not by editing the fallback.
|
||||||
5. Profile capped and uncapped production paths and retain evidence.
|
5. Profile capped and uncapped production paths and retain evidence.
|
||||||
|
|
||||||
Gate: identical framebuffer/scene digest, no per-frame PView input allocation,
|
Gate: identical framebuffer/scene digest, no per-frame PView input allocation,
|
||||||
and no diagnostic world traversal with all consumers disabled.
|
no iterator allocation for stable landblock views, and no shadow traversal
|
||||||
|
with all optional consumers disabled.
|
||||||
|
|
||||||
|
Landed evidence:
|
||||||
|
[`../research/2026-07-25-slice-h-a4-frame-scratch.md`](../research/2026-07-25-slice-h-a4-frame-scratch.md).
|
||||||
|
|
||||||
## 3. H-b — exact light top-k
|
## 3. H-b — exact light top-k
|
||||||
|
|
||||||
|
|
|
||||||
69
docs/research/2026-07-25-slice-h-a4-frame-scratch.md
Normal file
69
docs/research/2026-07-25-slice-h-a4-frame-scratch.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Slice H-a4 — retained frame scratch and diagnostic traversal
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This is a behavior-preserving ownership/allocation correction. It does not
|
||||||
|
change retail visibility, PView construction, spatial ordering, collision
|
||||||
|
debug drawing, title facts, or lifecycle checkpoint facts.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
Three independent steady-frame allocation sources remained:
|
||||||
|
|
||||||
|
1. `WorldSceneRenderer` constructed one `RetailPViewFrameInput` reference
|
||||||
|
object for every PView frame even though `DrawInside` consumes it
|
||||||
|
synchronously.
|
||||||
|
2. `GpuWorldState.LandblockEntries` and `LandblockBounds` were C# iterator
|
||||||
|
methods, so merely obtaining each sequence constructed fresh iterator
|
||||||
|
state. The underlying spatial membership changed only at update-thread
|
||||||
|
publication boundaries.
|
||||||
|
3. `WorldSceneDiagnosticsController` passed
|
||||||
|
`ShadowObjectRegistry.AllEntriesForDebug()` to the DebugVM publisher even
|
||||||
|
when no developer UI consumer existed. The publisher correctly avoided
|
||||||
|
enumeration, but the yield sequence itself had already been constructed.
|
||||||
|
|
||||||
|
The execution plan initially described landblock visibility counting as an
|
||||||
|
unconsumed diagnostic. That was incorrect in the current application:
|
||||||
|
`WorldRenderFrameOutcome` feeds the always-visible FPS/title `lb V/T` values
|
||||||
|
and connected lifecycle snapshots. Returning a neutral count would therefore
|
||||||
|
change observable behavior and invalidate lifecycle evidence. The count is
|
||||||
|
retained; only its iterator allocation is removed.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
- `WorldSceneRenderer` owns one `RetailPViewFrameInput` and resets every field
|
||||||
|
before the synchronous `DrawInside` call. The input is publicly read-only
|
||||||
|
and internally mutable so downstream render owners cannot change borrowed
|
||||||
|
frame facts.
|
||||||
|
- `GpuWorldState` exposes retained entry, no-animated-index entry, and bounds
|
||||||
|
lists. They rebuild only after landblock membership, resident entity-list,
|
||||||
|
animated-index, or AABB changes. Existing render-traversal slot order is
|
||||||
|
preserved exactly.
|
||||||
|
- Landblock diagnostic counting uses indexed access over the retained bounds
|
||||||
|
view.
|
||||||
|
- Shadow enumeration is constructed only when the DebugVM is mounted. The
|
||||||
|
independent collision-wireframe switch keeps its existing enumeration.
|
||||||
|
- `InteriorEntityPartition` remains untouched while the G4 referee is still
|
||||||
|
retained. Its removal and issue #241 close remain G5 work after the user
|
||||||
|
accepts the G4 visual gate.
|
||||||
|
|
||||||
|
## Conformance
|
||||||
|
|
||||||
|
- Stable retained render-view getters allocate zero bytes over 1,000 polls.
|
||||||
|
- A spatial mutation refreshes the same borrowed list identity and publishes
|
||||||
|
the new count.
|
||||||
|
- Two successive PView frames receive the same input object, reset with the
|
||||||
|
current frame values.
|
||||||
|
- Existing quiescence, render traversal, animated-index, PView executor,
|
||||||
|
abort/recovery, and DebugVM tests remain unchanged in behavior.
|
||||||
|
|
||||||
|
## Rollback boundary
|
||||||
|
|
||||||
|
This work is independent of the G4 production-source cutover. If tomorrow's
|
||||||
|
visual gate rejects G4, use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
git revert ef1d263337997bb030eadb7b8e71d73dc659907a
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not revert this Slice H-a4 commit, G3, or the portal-warmup corrections.
|
||||||
|
|
@ -1321,41 +1321,100 @@ internal sealed class BuildingGroupScratch
|
||||||
|
|
||||||
public sealed class RetailPViewFrameInput
|
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
|
/// <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>
|
/// 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 Vector3 ViewerEyePos { get; private set; }
|
||||||
public required Matrix4x4 ViewProjection { get; init; }
|
public Matrix4x4 ViewProjection { get; private set; }
|
||||||
public required IRetailPViewCellSource Cells { get; init; }
|
public IRetailPViewCellSource Cells { get; private set; } = null!;
|
||||||
public required ICamera Camera { get; init; }
|
public ICamera Camera { get; private set; } = null!;
|
||||||
public required Vector3 CameraWorldPosition { get; init; }
|
public Vector3 CameraWorldPosition { get; private set; }
|
||||||
public required FrustumPlanes? Frustum { get; init; }
|
public FrustumPlanes? Frustum { get; private set; }
|
||||||
public required uint? PlayerLandblockId { get; init; }
|
public uint? PlayerLandblockId { get; private set; }
|
||||||
public required HashSet<uint>? AnimatedEntityIds { get; init; }
|
public HashSet<uint>? AnimatedEntityIds { get; private set; }
|
||||||
public required int RenderCenterLbX { get; init; }
|
public int RenderCenterLbX { get; private set; }
|
||||||
public required int RenderCenterLbY { get; init; }
|
public int RenderCenterLbY { get; private set; }
|
||||||
public required int RenderRadius { get; init; }
|
public int RenderRadius { get; private set; }
|
||||||
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
IReadOnlyList<WorldEntity> Entities,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
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
|
// Pass-presentation and diagnostic values consumed synchronously by the
|
||||||
// typed executor. This input is data-only and is never retained.
|
// typed executor. This input is data-only and is never retained.
|
||||||
public required bool RenderSky { get; init; }
|
public bool RenderSky { get; private set; }
|
||||||
public required bool RenderWeather { get; init; }
|
public bool RenderWeather { get; private set; }
|
||||||
public required float DayFraction { get; init; }
|
public float DayFraction { get; private set; }
|
||||||
public required DayGroupData? ActiveDayGroup { get; init; }
|
public DayGroupData? ActiveDayGroup { get; private set; }
|
||||||
public required SkyKeyframe SkyKeyframe { get; init; }
|
public SkyKeyframe SkyKeyframe { get; private set; }
|
||||||
public required bool EnvironOverrideActive { get; init; }
|
public bool EnvironOverrideActive { get; private set; }
|
||||||
public required uint ViewerCellId { get; init; }
|
public uint ViewerCellId { get; private set; }
|
||||||
public required uint PlayerCellId { get; init; }
|
public uint PlayerCellId { get; private set; }
|
||||||
public required Vector3 PlayerViewPosition { get; init; }
|
public Vector3 PlayerViewPosition { get; private set; }
|
||||||
public required Matrix4x4 CameraView { get; init; }
|
public Matrix4x4 CameraView { get; private set; }
|
||||||
public required CameraCellResolution CameraCellResolution { get; init; }
|
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ internal interface IWorldSceneDiagnostics
|
||||||
|
|
||||||
WorldSceneDiagnosticOutcome DrawAndPublish(
|
WorldSceneDiagnosticOutcome DrawAndPublish(
|
||||||
in WorldCameraFrame camera,
|
in WorldCameraFrame camera,
|
||||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -168,16 +168,16 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
|
||||||
|
|
||||||
public WorldSceneDiagnosticOutcome DrawAndPublish(
|
public WorldSceneDiagnosticOutcome DrawAndPublish(
|
||||||
in WorldCameraFrame camera,
|
in WorldCameraFrame camera,
|
||||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(bounds);
|
ArgumentNullException.ThrowIfNull(bounds);
|
||||||
DrawCollisionWireframes(in camera);
|
DrawCollisionWireframes(in camera);
|
||||||
|
|
||||||
int visible = 0;
|
int visible = 0;
|
||||||
int total = 0;
|
int total = bounds.Count;
|
||||||
foreach (var entry in bounds)
|
for (int index = 0; index < bounds.Count; index++)
|
||||||
{
|
{
|
||||||
total++;
|
var entry = bounds[index];
|
||||||
if (FrustumCuller.IsAabbVisible(
|
if (FrustumCuller.IsAabbVisible(
|
||||||
camera.Frustum,
|
camera.Frustum,
|
||||||
entry.AabbMin,
|
entry.AabbMin,
|
||||||
|
|
@ -187,16 +187,21 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 nearestOrigin =
|
// AllEntriesForDebug is a yield sequence. Do not even construct its
|
||||||
_mode.IsPlayerMode && _player.Controller is { } controller
|
// iterator unless the developer UI is mounted and consumes the facts.
|
||||||
? controller.Position
|
if (_debugVmConsumerActive)
|
||||||
: camera.Position;
|
{
|
||||||
_debugVm.PublishDebugVmFacts(
|
Vector3 nearestOrigin =
|
||||||
_debugVmConsumerActive,
|
_mode.IsPlayerMode && _player.Controller is { } controller
|
||||||
visible,
|
? controller.Position
|
||||||
total,
|
: camera.Position;
|
||||||
nearestOrigin,
|
_debugVm.PublishDebugVmFacts(
|
||||||
_physics.ShadowObjects.AllEntriesForDebug());
|
consumerActive: true,
|
||||||
|
visible,
|
||||||
|
total,
|
||||||
|
nearestOrigin,
|
||||||
|
_physics.ShadowObjects.AllEntriesForDebug());
|
||||||
|
}
|
||||||
return new WorldSceneDiagnosticOutcome(visible, total);
|
return new WorldSceneDiagnosticOutcome(visible, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
||||||
private readonly IWorldRenderRangeSource _renderRange;
|
private readonly IWorldRenderRangeSource _renderRange;
|
||||||
private readonly IWorldSceneDiagnostics _diagnostics;
|
private readonly IWorldSceneDiagnostics _diagnostics;
|
||||||
private readonly IWorldGenerationAvailability _availability;
|
private readonly IWorldGenerationAvailability _availability;
|
||||||
|
private readonly RetailPViewFrameInput _pviewFrameInput = new();
|
||||||
|
|
||||||
public WorldSceneRenderer(
|
public WorldSceneRenderer(
|
||||||
IRenderFrameFoundationSource foundation,
|
IRenderFrameFoundationSource foundation,
|
||||||
|
|
@ -174,34 +175,32 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
||||||
{
|
{
|
||||||
pviewFrameStarted = true;
|
pviewFrameStarted = true;
|
||||||
pviewResult = _pview.DrawInside(
|
pviewResult = _pview.DrawInside(
|
||||||
new RetailPViewFrameInput
|
_pviewFrameInput.Reset(
|
||||||
{
|
clipRoot,
|
||||||
RootCell = clipRoot,
|
world.Buildings.NearbyBuildingCells,
|
||||||
NearbyBuildingCells = world.Buildings.NearbyBuildingCells,
|
roots.ViewerEyePosition,
|
||||||
ViewerEyePos = roots.ViewerEyePosition,
|
camera.ViewProjection,
|
||||||
ViewProjection = camera.ViewProjection,
|
_pviewCells,
|
||||||
Cells = _pviewCells,
|
camera.Camera,
|
||||||
Camera = camera.Camera,
|
camera.Position,
|
||||||
CameraWorldPosition = camera.Position,
|
camera.Frustum,
|
||||||
Frustum = camera.Frustum,
|
roots.PlayerLandblockId,
|
||||||
PlayerLandblockId = roots.PlayerLandblockId,
|
world.AnimatedEntityIds,
|
||||||
AnimatedEntityIds = world.AnimatedEntityIds,
|
roots.RenderCenterLandblockX,
|
||||||
RenderCenterLbX = roots.RenderCenterLandblockX,
|
roots.RenderCenterLandblockY,
|
||||||
RenderCenterLbY = roots.RenderCenterLandblockY,
|
_renderRange.NearRadius,
|
||||||
RenderRadius = _renderRange.NearRadius,
|
_entities.LandblockEntries,
|
||||||
LandblockEntries = _entities.LandblockEntries,
|
renderSky,
|
||||||
RenderSky = renderSky,
|
roots.PlayerSeenOutside,
|
||||||
RenderWeather = roots.PlayerSeenOutside,
|
_sky.DayFraction,
|
||||||
DayFraction = _sky.DayFraction,
|
_sky.ActiveDayGroup,
|
||||||
ActiveDayGroup = _sky.ActiveDayGroup,
|
foundation.Sky,
|
||||||
SkyKeyframe = foundation.Sky,
|
foundation.EnvironOverrideActive,
|
||||||
EnvironOverrideActive = foundation.EnvironOverrideActive,
|
roots.ViewerCellId,
|
||||||
ViewerCellId = roots.ViewerCellId,
|
roots.PlayerCellId,
|
||||||
PlayerCellId = roots.PlayerCellId,
|
roots.PlayerViewPosition,
|
||||||
PlayerViewPosition = roots.PlayerViewPosition,
|
camera.Camera.View,
|
||||||
CameraView = camera.Camera.View,
|
_diagnostics.CameraCellResolution));
|
||||||
CameraCellResolution = _diagnostics.CameraCellResolution,
|
|
||||||
});
|
|
||||||
|
|
||||||
_particleVisibility.MarkVisibleCells(pviewResult.DrawableCells);
|
_particleVisibility.MarkVisibleCells(pviewResult.DrawableCells);
|
||||||
_frames.ObserveDrawableCells(pviewResult.DrawableCells);
|
_frames.ObserveDrawableCells(pviewResult.DrawableCells);
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ internal interface IWorldSceneSkyStateSource
|
||||||
|
|
||||||
internal interface IWorldSceneEntitySource
|
internal interface IWorldSceneEntitySource
|
||||||
{
|
{
|
||||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
IReadOnlyList<WorldEntity> Entities,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
||||||
{ get; }
|
{ get; }
|
||||||
|
|
||||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds { get; }
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class RuntimeWorldSceneEntitySource : IWorldSceneEntitySource
|
internal sealed class RuntimeWorldSceneEntitySource : IWorldSceneEntitySource
|
||||||
|
|
@ -32,12 +32,12 @@ internal sealed class RuntimeWorldSceneEntitySource : IWorldSceneEntitySource
|
||||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
_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,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
|
||||||
_world.LandblockEntries;
|
_world.LandblockEntries;
|
||||||
|
|
||||||
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
|
||||||
_world.LandblockBounds;
|
_world.LandblockBounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,21 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
private readonly Dictionary<uint, LandblockStreamTier> _tierByLandblock = new();
|
private readonly Dictionary<uint, LandblockStreamTier> _tierByLandblock = new();
|
||||||
private readonly Dictionary<uint, (Vector3 Min, Vector3 Max)> _aabbs = new();
|
private readonly Dictionary<uint, (Vector3 Min, Vector3 Max)> _aabbs = new();
|
||||||
private readonly Dictionary<uint, AnimatedEntityIndex> _animatedIndexByLandblock = new();
|
private readonly Dictionary<uint, AnimatedEntityIndex> _animatedIndexByLandblock = new();
|
||||||
|
// H-a4: synchronous render borrows. The former yield properties created
|
||||||
|
// fresh iterator state at every per-frame call site.
|
||||||
|
private readonly List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>
|
||||||
|
_landblockEntriesView = [];
|
||||||
|
private readonly List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>
|
||||||
|
_landblockEntriesWithoutAnimatedIndexView = [];
|
||||||
|
private readonly List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)>
|
||||||
|
_landblockBoundsView = [];
|
||||||
|
private bool _landblockEntriesViewDirty = true;
|
||||||
|
private bool _landblockEntriesWithoutAnimatedIndexViewDirty = true;
|
||||||
|
private bool _landblockBoundsViewDirty = true;
|
||||||
|
|
||||||
private sealed record AnimatedEntityIndex(
|
private sealed record AnimatedEntityIndex(
|
||||||
LoadedLandblock Source,
|
LoadedLandblock Source,
|
||||||
|
|
@ -201,6 +216,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
public void SetLandblockAabb(uint landblockId, Vector3 min, Vector3 max)
|
public void SetLandblockAabb(uint landblockId, Vector3 min, Vector3 max)
|
||||||
{
|
{
|
||||||
_aabbs[landblockId] = (min, max);
|
_aabbs[landblockId] = (min, max);
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -219,49 +235,77 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
/// explicitly invalidate exactly the affected index generation.
|
/// explicitly invalidate exactly the affected index generation.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
IReadOnlyList<WorldEntity> Entities,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
|
||||||
=> EnumerateLandblockEntries(includeAnimatedIndex: true);
|
=> GetLandblockEntriesView(includeAnimatedIndex: true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-landblock render entries without the animated lookup dictionary.
|
/// Per-landblock render entries without the animated lookup dictionary.
|
||||||
/// Static render passes use this to avoid rebuilding an index they cannot
|
/// Static render passes use this to avoid rebuilding an index they cannot
|
||||||
/// consume.
|
/// consume.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
IReadOnlyList<WorldEntity> Entities,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntriesWithoutAnimatedIndex
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntriesWithoutAnimatedIndex
|
||||||
=> EnumerateLandblockEntries(includeAnimatedIndex: false);
|
=> GetLandblockEntriesView(includeAnimatedIndex: false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lightweight bounds-only enumeration for overlays and diagnostics.
|
/// Lightweight bounds-only enumeration for overlays and diagnostics.
|
||||||
/// Does not walk entity lists.
|
/// Does not walk entity lists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!_availability.IsWorldAvailable)
|
if (!_availability.IsWorldAvailable)
|
||||||
yield break;
|
return Array.Empty<(uint, Vector3, Vector3)>();
|
||||||
|
if (!_landblockBoundsViewDirty)
|
||||||
|
return _landblockBoundsView;
|
||||||
|
|
||||||
foreach (var kvp in _loaded)
|
_landblockBoundsView.Clear();
|
||||||
|
for (int slot = 0; slot < _renderTraversalLandblockSlots.Count; slot++)
|
||||||
{
|
{
|
||||||
if (_aabbs.TryGetValue(kvp.Key, out var aabb))
|
uint landblockId = _renderTraversalLandblockSlots[slot];
|
||||||
yield return (kvp.Key, aabb.Min, aabb.Max);
|
if (landblockId == 0)
|
||||||
|
continue;
|
||||||
|
if (_aabbs.TryGetValue(landblockId, out var aabb))
|
||||||
|
_landblockBoundsView.Add((landblockId, aabb.Min, aabb.Max));
|
||||||
else
|
else
|
||||||
yield return (kvp.Key, Vector3.Zero, Vector3.Zero);
|
_landblockBoundsView.Add(
|
||||||
|
(landblockId, Vector3.Zero, Vector3.Zero));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_landblockBoundsViewDirty = false;
|
||||||
|
return _landblockBoundsView;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
private IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
IReadOnlyList<WorldEntity> Entities,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> EnumerateLandblockEntries(
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> GetLandblockEntriesView(
|
||||||
bool includeAnimatedIndex)
|
bool includeAnimatedIndex)
|
||||||
{
|
{
|
||||||
if (!_availability.IsWorldAvailable)
|
if (!_availability.IsWorldAvailable)
|
||||||
yield break;
|
{
|
||||||
|
return Array.Empty<(uint, Vector3, Vector3,
|
||||||
|
IReadOnlyList<WorldEntity>,
|
||||||
|
IReadOnlyDictionary<uint, WorldEntity>?)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> view =
|
||||||
|
includeAnimatedIndex
|
||||||
|
? _landblockEntriesView
|
||||||
|
: _landblockEntriesWithoutAnimatedIndexView;
|
||||||
|
bool dirty = includeAnimatedIndex
|
||||||
|
? _landblockEntriesViewDirty
|
||||||
|
: _landblockEntriesWithoutAnimatedIndexViewDirty;
|
||||||
|
if (!dirty)
|
||||||
|
return view;
|
||||||
|
|
||||||
|
view.Clear();
|
||||||
|
|
||||||
for (int slot = 0; slot < _renderTraversalLandblockSlots.Count; slot++)
|
for (int slot = 0; slot < _renderTraversalLandblockSlots.Count; slot++)
|
||||||
{
|
{
|
||||||
|
|
@ -287,10 +331,29 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_aabbs.TryGetValue(landblockId, out var aabb))
|
if (_aabbs.TryGetValue(landblockId, out var aabb))
|
||||||
yield return (landblockId, aabb.Min, aabb.Max, landblock.Entities, byId);
|
view.Add(
|
||||||
|
(landblockId, aabb.Min, aabb.Max, landblock.Entities, byId));
|
||||||
else
|
else
|
||||||
yield return (landblockId, Vector3.Zero, Vector3.Zero, landblock.Entities, byId);
|
view.Add(
|
||||||
|
(landblockId, Vector3.Zero, Vector3.Zero, landblock.Entities, byId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeAnimatedIndex)
|
||||||
|
_landblockEntriesViewDirty = false;
|
||||||
|
else
|
||||||
|
_landblockEntriesWithoutAnimatedIndexViewDirty = false;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvalidateLandblockRenderViews(bool entries, bool bounds)
|
||||||
|
{
|
||||||
|
if (entries)
|
||||||
|
{
|
||||||
|
_landblockEntriesViewDirty = true;
|
||||||
|
_landblockEntriesWithoutAnimatedIndexViewDirty = true;
|
||||||
|
}
|
||||||
|
if (bounds)
|
||||||
|
_landblockBoundsViewDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool TryGetRenderTraversalSortKey(
|
internal bool TryGetRenderTraversalSortKey(
|
||||||
|
|
@ -361,6 +424,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Landblock 0x{landblockId:X8} already owns a render traversal slot.");
|
$"Landblock 0x{landblockId:X8} already owns a render traversal slot.");
|
||||||
}
|
}
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RemoveLoadedLandblock(
|
private bool RemoveLoadedLandblock(
|
||||||
|
|
@ -381,6 +445,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
_renderTraversalLandblockSlots[slot] = 0;
|
_renderTraversalLandblockSlots[slot] = 0;
|
||||||
_freeRenderTraversalLandblockSlots.Push(slot);
|
_freeRenderTraversalLandblockSlots.Push(slot);
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -726,6 +791,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
int bucketIndex = entities.Count;
|
int bucketIndex = entities.Count;
|
||||||
entities.Add(entity);
|
entities.Add(entity);
|
||||||
_animatedIndexByLandblock.Remove(landblockId);
|
_animatedIndexByLandblock.Remove(landblockId);
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
AddFlatEntity(entity);
|
AddFlatEntity(entity);
|
||||||
SetProjectionLocation(entity, landblockId, isLoaded: true, bucketIndex);
|
SetProjectionLocation(entity, landblockId, isLoaded: true, bucketIndex);
|
||||||
AdjustVisibleLiveProjection(entity, +1);
|
AdjustVisibleLiveProjection(entity, +1);
|
||||||
|
|
@ -765,6 +831,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
entities.RemoveAt(lastIndex);
|
entities.RemoveAt(lastIndex);
|
||||||
|
|
||||||
_animatedIndexByLandblock.Remove(location.LandblockId);
|
_animatedIndexByLandblock.Remove(location.LandblockId);
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
RemoveFlatEntity(entity);
|
RemoveFlatEntity(entity);
|
||||||
RemoveProjectionLocation(entity);
|
RemoveProjectionLocation(entity);
|
||||||
AdjustVisibleLiveProjection(entity, -1);
|
AdjustVisibleLiveProjection(entity, -1);
|
||||||
|
|
@ -1463,6 +1530,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
}
|
}
|
||||||
_loaded[canonical] = NormalizeLoadedLandblock(lb, retainedLive);
|
_loaded[canonical] = NormalizeLoadedLandblock(lb, retainedLive);
|
||||||
_animatedIndexByLandblock.Remove(canonical);
|
_animatedIndexByLandblock.Remove(canonical);
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
_tierByLandblock[canonical] = LandblockStreamTier.Far;
|
_tierByLandblock[canonical] = LandblockStreamTier.Far;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1595,6 +1663,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
AdjustVisibleLiveProjection(entity, +1);
|
AdjustVisibleLiveProjection(entity, +1);
|
||||||
}
|
}
|
||||||
_animatedIndexByLandblock.Remove(canonical);
|
_animatedIndexByLandblock.Remove(canonical);
|
||||||
|
InvalidateLandblockRenderViews(entries: true, bounds: false);
|
||||||
_tierByLandblock[canonical] = LandblockStreamTier.Near;
|
_tierByLandblock[canonical] = LandblockStreamTier.Near;
|
||||||
ulong[] effectiveRenderIds = additionalRenderIds?.ToArray() ?? Array.Empty<ulong>();
|
ulong[] effectiveRenderIds = additionalRenderIds?.ToArray() ?? Array.Empty<ulong>();
|
||||||
WorldEntity[] staticEntities = entities
|
WorldEntity[] staticEntities = entities
|
||||||
|
|
|
||||||
|
|
@ -418,34 +418,32 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
null));
|
null));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RetailPViewFrameInput
|
return new RetailPViewFrameInput().Reset(
|
||||||
{
|
root,
|
||||||
RootCell = root,
|
nearbyBuildingCells,
|
||||||
NearbyBuildingCells = nearbyBuildingCells,
|
Vector3.Zero,
|
||||||
ViewerEyePos = Vector3.Zero,
|
TestCamera.ViewProjection,
|
||||||
ViewProjection = TestCamera.ViewProjection,
|
new DictionaryCellSource(cells),
|
||||||
Cells = new DictionaryCellSource(cells),
|
new TestCamera(),
|
||||||
Camera = new TestCamera(),
|
Vector3.Zero,
|
||||||
CameraWorldPosition = Vector3.Zero,
|
frustum: null,
|
||||||
Frustum = null,
|
playerLandblockId: root.CellId & 0xFFFF0000u,
|
||||||
PlayerLandblockId = root.CellId & 0xFFFF0000u,
|
animatedEntityIds: null,
|
||||||
AnimatedEntityIds = null,
|
renderCenterLbX: 0,
|
||||||
RenderCenterLbX = 0,
|
renderCenterLbY: 0,
|
||||||
RenderCenterLbY = 0,
|
renderRadius: 1,
|
||||||
RenderRadius = 1,
|
landblockEntries: entries,
|
||||||
LandblockEntries = entries,
|
renderSky: true,
|
||||||
RenderSky = true,
|
renderWeather: true,
|
||||||
RenderWeather = true,
|
dayFraction: 0f,
|
||||||
DayFraction = 0f,
|
activeDayGroup: null,
|
||||||
ActiveDayGroup = null,
|
skyKeyframe: default,
|
||||||
SkyKeyframe = default,
|
environOverrideActive: false,
|
||||||
EnvironOverrideActive = false,
|
viewerCellId: root.CellId,
|
||||||
ViewerCellId = root.CellId,
|
playerCellId: root.CellId,
|
||||||
PlayerCellId = root.CellId,
|
playerViewPosition: Vector3.Zero,
|
||||||
PlayerViewPosition = Vector3.Zero,
|
cameraView: TestCamera.ViewMatrix,
|
||||||
CameraView = TestCamera.ViewMatrix,
|
cameraCellResolution: CameraCellResolution.None);
|
||||||
CameraCellResolution = CameraCellResolution.None,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LoadedCell InteriorWithExit(uint cellId)
|
private static LoadedCell InteriorWithExit(uint cellId)
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,32 @@ public sealed class WorldSceneRendererTests
|
||||||
Assert.Equal(4, rig.PView.LastInput.RenderRadius);
|
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]
|
[Fact]
|
||||||
public void OutdoorPView_UsesPViewOwnersForPostWorldParticlesWithoutFlatWeather()
|
public void OutdoorPView_UsesPViewOwnersForPostWorldParticlesWithoutFlatWeather()
|
||||||
{
|
{
|
||||||
|
|
@ -474,13 +500,13 @@ public sealed class WorldSceneRendererTests
|
||||||
|
|
||||||
private sealed class EntitySource : IWorldSceneEntitySource
|
private sealed class EntitySource : IWorldSceneEntitySource
|
||||||
{
|
{
|
||||||
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
IReadOnlyList<WorldEntity> Entities,
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
|
||||||
Array.Empty<(uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
Array.Empty<(uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
||||||
IReadOnlyDictionary<uint, 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)>();
|
Array.Empty<(uint, Vector3, Vector3)>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -697,7 +723,7 @@ public sealed class WorldSceneRendererTests
|
||||||
|
|
||||||
public WorldSceneDiagnosticOutcome DrawAndPublish(
|
public WorldSceneDiagnosticOutcome DrawAndPublish(
|
||||||
in WorldCameraFrame camera,
|
in WorldCameraFrame camera,
|
||||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
|
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
|
||||||
{
|
{
|
||||||
calls.Add("diagnostics:draw");
|
calls.Add("diagnostics:draw");
|
||||||
return new WorldSceneDiagnosticOutcome(3, 8);
|
return new WorldSceneDiagnosticOutcome(3, 8);
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,44 @@ public sealed class GpuWorldStateRenderTraversalTests
|
||||||
Assert.False(state.TryGetRenderTraversalSortKey(pending, out _));
|
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(
|
private static void AssertSort(
|
||||||
GpuWorldState state,
|
GpuWorldState state,
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue