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,