perf(streaming): scope reveal warmup spatially

Source composite warmup from the canonical published destination neighborhood, including quiesced static and live projections, instead of rescanning the retained Far-tier world after every membership edge.

This lifecycle correction applies to both draw paths, preserving ef1d263337 as the sole G4 visual rollback.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 04:39:48 +02:00
parent 129dd77ddd
commit 03b10183e9
5 changed files with 212 additions and 6 deletions

View file

@ -327,17 +327,27 @@ internal sealed class SessionPlayerCompositionPhase
d.Selection,
live.WorldState,
content.Audio?.Engine);
var compositeWarmupSource =
new CompositeWarmupEntitySource(live.WorldState);
var worldReveal = new WorldRevealCoordinator(
streaming.IsRenderNeighborhoodResident,
d.PhysicsEngine.IsSpawnCellReady,
d.PhysicsEngine.IsNeighborhoodTerrainResident,
() => live.DrawDispatcher.CompositeTexturesReady,
(destinationCell, radius) => live.DrawDispatcher.PrepareCompositeTextures(
live.WorldState.Entities,
live.WorldState.FlatViewGeneration,
destinationCell,
radius),
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
(destinationCell, radius) =>
{
compositeWarmupSource.Refresh(destinationCell, radius);
live.DrawDispatcher.PrepareCompositeTextures(
compositeWarmupSource.Entities,
compositeWarmupSource.Generation,
destinationCell,
radius);
},
() =>
{
compositeWarmupSource.Reset();
live.DrawDispatcher.InvalidateCompositeWarmupReadiness();
},
spawnClaimClassifier.IsUnhydratable,
d.Log,
worldQuiescence,

View file

@ -0,0 +1,59 @@
using AcDream.App.Streaming;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Retains the exact published destination-neighborhood snapshot consumed by
/// composite-texture reveal warmup. The source refreshes only when canonical
/// spatial membership or its destination scope changes, avoiding a scan of
/// the retained Far-tier world on every reveal tick.
/// </summary>
internal sealed class CompositeWarmupEntitySource
{
private readonly GpuWorldState _world;
private readonly List<WorldEntity> _entities = [];
private bool _initialized;
private ulong _worldGeneration;
private uint _destinationCell;
private int _radius;
public CompositeWarmupEntitySource(GpuWorldState world)
{
_world = world ?? throw new ArgumentNullException(nameof(world));
}
public IReadOnlyList<WorldEntity> Entities => _entities;
public ulong Generation => _worldGeneration;
public void Refresh(uint destinationCell, int radius)
{
ArgumentOutOfRangeException.ThrowIfNegative(radius);
ulong worldGeneration = _world.FlatViewGeneration;
if (_initialized
&& _worldGeneration == worldGeneration
&& _destinationCell == destinationCell
&& _radius == radius)
{
return;
}
_world.CopyPublishedEntitiesNearLandblockForReadiness(
destinationCell,
radius,
_entities);
_worldGeneration = worldGeneration;
_destinationCell = destinationCell;
_radius = radius;
_initialized = true;
}
public void Reset()
{
_entities.Clear();
_initialized = false;
_worldGeneration = 0;
_destinationCell = 0;
_radius = 0;
}
}

View file

@ -449,6 +449,46 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
}
}
/// <summary>
/// Copies every published projection from a bounded destination
/// neighborhood for presentation-readiness work. Unlike gameplay-facing
/// spatial queries, this remains available while the world generation is
/// quiesced: the reveal barrier must prepare destination resources before
/// it makes that generation observable.
/// </summary>
internal void CopyPublishedEntitiesNearLandblockForReadiness(
uint centerCellOrLandblockId,
int landblockRadius,
List<WorldEntity> destination)
{
ArgumentNullException.ThrowIfNull(destination);
ArgumentOutOfRangeException.ThrowIfNegative(landblockRadius);
destination.Clear();
int centerX = (int)((centerCellOrLandblockId >> 24) & 0xFFu);
int centerY = (int)((centerCellOrLandblockId >> 16) & 0xFFu);
for (int dx = -landblockRadius; dx <= landblockRadius; dx++)
for (int dy = -landblockRadius; dy <= landblockRadius; dy++)
{
int x = centerX + dx;
int y = centerY + dy;
if ((uint)x > 0xFFu || (uint)y > 0xFFu)
continue;
uint landblockId =
((uint)x << 24) | ((uint)y << 16) | 0xFFFFu;
if (!_loaded.TryGetValue(
landblockId,
out LoadedLandblock? landblock))
{
continue;
}
for (int i = 0; i < landblock.Entities.Count; i++)
destination.Add(landblock.Entities[i]);
}
}
public readonly ref struct MutationBatch
{
private readonly GpuWorldState _owner;

View file

@ -0,0 +1,61 @@
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using System.Numerics;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class CompositeWarmupEntitySourceTests
{
[Fact]
public void RefreshTracksOnlyDestinationNeighborhoodMembership()
{
var center = Entity(1);
var neighbor = Entity(2);
var far = Entity(3);
var world = new GpuWorldState();
world.AddLandblock(Landblock(0x1010FFFFu, center));
world.AddLandblock(Landblock(0x1110FFFFu, neighbor));
world.AddLandblock(Landblock(0x1310FFFFu, far));
var source = new CompositeWarmupEntitySource(world);
source.Refresh(0x10100001u, radius: 1);
Assert.Equal(world.FlatViewGeneration, source.Generation);
Assert.Equal([center, neighbor], source.Entities);
}
[Fact]
public void RefreshObservesLaterMembershipWithoutReplacingItsStableView()
{
var center = Entity(1);
var later = Entity(2);
var world = new GpuWorldState();
world.AddLandblock(Landblock(0x1010FFFFu, center));
var source = new CompositeWarmupEntitySource(world);
source.Refresh(0x10100001u, radius: 0);
IReadOnlyList<WorldEntity> stableView = source.Entities;
world.AddLandblock(Landblock(0x1010FFFFu, center, later));
source.Refresh(0x10100001u, radius: 0);
Assert.Same(stableView, source.Entities);
Assert.Equal([center, later], source.Entities);
Assert.Equal(world.FlatViewGeneration, source.Generation);
}
private static LoadedLandblock Landblock(
uint id,
params WorldEntity[] entities) =>
new(id, new LandBlock(), entities);
private static WorldEntity Entity(uint id) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
}

View file

@ -508,6 +508,42 @@ public sealed class GpuWorldStateVisibilityTests
Assert.DoesNotContain(destination, pair => pair.Key == 0u);
}
[Fact]
public void ReadinessNeighborhood_CopiesStaticAndLiveWhileWorldIsQuiesced()
{
var availability = new WorldGenerationAvailabilityState();
availability.Begin(7);
var centerStatic = Entity(1, 0u);
var centerLive = Entity(2, 0x70000001u);
var neighborStatic = Entity(3, 0u);
var farStatic = Entity(4, 0u);
var state = new GpuWorldState(availability: availability);
state.AddLandblock(new LoadedLandblock(
0x1010FFFFu,
new LandBlock(),
new[] { centerStatic, centerLive }));
state.AddLandblock(new LoadedLandblock(
0x1110FFFFu,
new LandBlock(),
new[] { neighborStatic }));
state.AddLandblock(new LoadedLandblock(
0x1310FFFFu,
new LandBlock(),
new[] { farStatic }));
var destination = new List<WorldEntity> { farStatic };
state.CopyPublishedEntitiesNearLandblockForReadiness(
0x10100001u,
1,
destination);
Assert.Equal(3, destination.Count);
Assert.Contains(centerStatic, destination);
Assert.Contains(centerLive, destination);
Assert.Contains(neighborStatic, destination);
Assert.DoesNotContain(farStatic, destination);
}
private static WorldEntity Entity(uint id, uint guid) => new()
{
Id = id,