diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs
index aae39e74..6cabe84b 100644
--- a/src/AcDream.App/Composition/SessionPlayerComposition.cs
+++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs
@@ -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,
diff --git a/src/AcDream.App/Rendering/Wb/CompositeWarmupEntitySource.cs b/src/AcDream.App/Rendering/Wb/CompositeWarmupEntitySource.cs
new file mode 100644
index 00000000..1f940f1c
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/CompositeWarmupEntitySource.cs
@@ -0,0 +1,59 @@
+using AcDream.App.Streaming;
+using AcDream.Core.World;
+
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// 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.
+///
+internal sealed class CompositeWarmupEntitySource
+{
+ private readonly GpuWorldState _world;
+ private readonly List _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 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;
+ }
+}
diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs
index 28cbab3b..b5ddb6d3 100644
--- a/src/AcDream.App/Streaming/GpuWorldState.cs
+++ b/src/AcDream.App/Streaming/GpuWorldState.cs
@@ -449,6 +449,46 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
}
}
+ ///
+ /// 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.
+ ///
+ internal void CopyPublishedEntitiesNearLandblockForReadiness(
+ uint centerCellOrLandblockId,
+ int landblockRadius,
+ List 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;
diff --git a/tests/AcDream.App.Tests/Rendering/Wb/CompositeWarmupEntitySourceTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/CompositeWarmupEntitySourceTests.cs
new file mode 100644
index 00000000..7eb0a75a
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Wb/CompositeWarmupEntitySourceTests.cs
@@ -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 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(),
+ };
+}
diff --git a/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs b/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs
index 1de2c8a4..e804f4c8 100644
--- a/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs
+++ b/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs
@@ -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 { 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,