fix(streaming): prepare quiesced destination entities
Separate loaded spatial residency from the world presentation gate so destination live objects acquire mesh owners behind portal space while drawing and simulation stay quiesced. Prevent unowned CPU-cache hits from recreating stale GPU staging work. Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
parent
e991eeca34
commit
91e82c3c68
6 changed files with 142 additions and 23 deletions
|
|
@ -8,6 +8,7 @@ internal enum MeshStageResult
|
||||||
Staged,
|
Staged,
|
||||||
AlreadyStaged,
|
AlreadyStaged,
|
||||||
HighWater,
|
HighWater,
|
||||||
|
NotOwned,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -282,7 +283,7 @@ internal sealed class CpuMeshUploadCache
|
||||||
// review). Read via Stats from the diagnostics/checkpoint thread
|
// review). Read via Stats from the diagnostics/checkpoint thread
|
||||||
// without taking _data's lock, so plain fields with Interlocked
|
// without taking _data's lock, so plain fields with Interlocked
|
||||||
// increments — the 4 concurrent preparation workers all consult
|
// increments — the 4 concurrent preparation workers all consult
|
||||||
// TryGetAndStage.
|
// TryGetAndStageOwned.
|
||||||
private long _hits;
|
private long _hits;
|
||||||
private long _misses;
|
private long _misses;
|
||||||
private long _evictions;
|
private long _evictions;
|
||||||
|
|
@ -307,12 +308,15 @@ internal sealed class CpuMeshUploadCache
|
||||||
internal int Capacity => _capacity;
|
internal int Capacity => _capacity;
|
||||||
internal long ByteCapacity => _byteCapacity;
|
internal long ByteCapacity => _byteCapacity;
|
||||||
|
|
||||||
public bool TryGetAndStage(
|
public bool TryGetAndStageOwned(
|
||||||
ulong id,
|
ulong id,
|
||||||
MeshUploadStagingQueue staging,
|
MeshUploadStagingQueue staging,
|
||||||
|
MeshOwnershipCounter ownership,
|
||||||
out ObjectMeshData? data,
|
out ObjectMeshData? data,
|
||||||
out MeshStageResult stageResult)
|
out MeshStageResult stageResult)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(staging);
|
||||||
|
ArgumentNullException.ThrowIfNull(ownership);
|
||||||
lock (_data)
|
lock (_data)
|
||||||
{
|
{
|
||||||
if (!_data.TryGetValue(id, out data)) {
|
if (!_data.TryGetValue(id, out data)) {
|
||||||
|
|
@ -323,7 +327,13 @@ internal sealed class CpuMeshUploadCache
|
||||||
Interlocked.Increment(ref _hits);
|
Interlocked.Increment(ref _hits);
|
||||||
_lru.Remove(id);
|
_lru.Remove(id);
|
||||||
_lru.AddLast(id);
|
_lru.AddLast(id);
|
||||||
stageResult = staging.TryStage(data);
|
// Prepared payload residency is deliberately independent from
|
||||||
|
// live render ownership. A point-of-use lookup from a retiring
|
||||||
|
// world generation must never recreate GPU staging work after
|
||||||
|
// that generation released its final mesh reference.
|
||||||
|
stageResult = ownership.IsOwned(id)
|
||||||
|
? staging.TryStage(data)
|
||||||
|
: MeshStageResult.NotOwned;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -876,9 +876,10 @@ namespace AcDream.App.Rendering.Wb
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectMeshData? deferredCachedData = null;
|
ObjectMeshData? deferredCachedData = null;
|
||||||
if (_cpuMeshCache.TryGetAndStage(
|
if (_cpuMeshCache.TryGetAndStageOwned(
|
||||||
geomId,
|
geomId,
|
||||||
_stagedMeshData,
|
_stagedMeshData,
|
||||||
|
_ownership,
|
||||||
out ObjectMeshData? cachedData,
|
out ObjectMeshData? cachedData,
|
||||||
out MeshStageResult cacheStage))
|
out MeshStageResult cacheStage))
|
||||||
{
|
{
|
||||||
|
|
@ -936,9 +937,10 @@ namespace AcDream.App.Rendering.Wb
|
||||||
_terminalPreparationFailures.Remove(id);
|
_terminalPreparationFailures.Remove(id);
|
||||||
|
|
||||||
ObjectMeshData? deferredCachedData = null;
|
ObjectMeshData? deferredCachedData = null;
|
||||||
if (_cpuMeshCache.TryGetAndStage(
|
if (_cpuMeshCache.TryGetAndStageOwned(
|
||||||
id,
|
id,
|
||||||
_stagedMeshData,
|
_stagedMeshData,
|
||||||
|
_ownership,
|
||||||
out ObjectMeshData? cachedData,
|
out ObjectMeshData? cachedData,
|
||||||
out MeshStageResult cacheStage))
|
out MeshStageResult cacheStage))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
public bool IsRenderReady(uint landblockId) =>
|
public bool IsRenderReady(uint landblockId) =>
|
||||||
_loaded.ContainsKey(landblockId)
|
_loaded.ContainsKey(landblockId)
|
||||||
&& (_wbSpawnAdapter?.IsLandblockRenderReady(landblockId) ?? true);
|
&& (_wbSpawnAdapter?.IsLandblockRenderReady(landblockId) ?? true);
|
||||||
|
/// <summary>
|
||||||
|
/// True when at least one projection for the server object belongs to a
|
||||||
|
/// loaded spatial bucket. This deliberately ignores the world-generation
|
||||||
|
/// presentation gate: destination objects must acquire their render
|
||||||
|
/// resources while portal/login reveal keeps normal world consumers closed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLiveEntityProjectionResident(uint serverGuid) =>
|
||||||
|
serverGuid != 0
|
||||||
|
&& _visibleLiveProjectionCounts.ContainsKey(serverGuid);
|
||||||
|
|
||||||
public bool IsLiveEntityVisible(uint serverGuid) =>
|
public bool IsLiveEntityVisible(uint serverGuid) =>
|
||||||
_availability.IsWorldAvailable
|
_availability.IsWorldAvailable
|
||||||
&& serverGuid != 0
|
&& IsLiveEntityProjectionResident(serverGuid);
|
||||||
&& _visibleLiveProjectionCounts.ContainsKey(serverGuid);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to grab the loaded record for a landblock — useful for callers
|
/// Try to grab the loaded record for a landblock — useful for callers
|
||||||
|
|
|
||||||
|
|
@ -843,7 +843,7 @@ public sealed class LiveEntityRuntime
|
||||||
runtimeNotificationFailure: null);
|
runtimeNotificationFailure: null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool visible = _spatial.IsLiveEntityVisible(serverGuid);
|
bool visible = _spatial.IsLiveEntityProjectionResident(serverGuid);
|
||||||
record.IsSpatiallyVisible = visible;
|
record.IsSpatiallyVisible = visible;
|
||||||
if (record.ProjectionKind is LiveEntityProjectionKind.World)
|
if (record.ProjectionKind is LiveEntityProjectionKind.World)
|
||||||
_materializedWorldEntitiesByGuid[serverGuid] = entity;
|
_materializedWorldEntitiesByGuid[serverGuid] = entity;
|
||||||
|
|
@ -2584,7 +2584,7 @@ public sealed class LiveEntityRuntime
|
||||||
// an older queued edge drains. Apply an edge only while it still
|
// an older queued edge drains. Apply an edge only while it still
|
||||||
// matches current spatial truth; otherwise it belongs to the displaced
|
// matches current spatial truth; otherwise it belongs to the displaced
|
||||||
// projection and must not mutate the replacement generation.
|
// projection and must not mutate the replacement generation.
|
||||||
if (_spatial.IsLiveEntityVisible(serverGuid) != visible)
|
if (_spatial.IsLiveEntityProjectionResident(serverGuid) != visible)
|
||||||
return;
|
return;
|
||||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||||
|| record.WorldEntity is not { } entity)
|
|| record.WorldEntity is not { } entity)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ public sealed class MeshUploadCachesTests
|
||||||
{
|
{
|
||||||
var staging = new MeshUploadStagingQueue();
|
var staging = new MeshUploadStagingQueue();
|
||||||
var cache = new CpuMeshUploadCache(capacity: 2);
|
var cache = new CpuMeshUploadCache(capacity: 2);
|
||||||
|
var ownership = Owned(0x01000010ul);
|
||||||
byte[] textureBytes = [1, 2, 3, 4];
|
byte[] textureBytes = [1, 2, 3, 4];
|
||||||
var data = new ObjectMeshData { ObjectId = 0x01000010ul, UploadAttempts = 2 };
|
var data = new ObjectMeshData { ObjectId = 0x01000010ul, UploadAttempts = 2 };
|
||||||
data.TextureBatches[(1, 1, TextureFormat.RGBA8)] =
|
data.TextureBatches[(1, 1, TextureFormat.RGBA8)] =
|
||||||
|
|
@ -25,11 +26,13 @@ public sealed class MeshUploadCachesTests
|
||||||
staging.Complete(initial); // GPU upload completed, then was later evicted.
|
staging.Complete(initial); // GPU upload completed, then was later evicted.
|
||||||
|
|
||||||
data.UploadAttempts = 3;
|
data.UploadAttempts = 3;
|
||||||
Assert.True(cache.TryGetAndStage(data.ObjectId, staging, out var cached, out _));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
data.ObjectId, staging, ownership, out var cached, out _));
|
||||||
Assert.Same(data, cached);
|
Assert.Same(data, cached);
|
||||||
Assert.Equal(0, data.UploadAttempts);
|
Assert.Equal(0, data.UploadAttempts);
|
||||||
Assert.Equal(textureBytes, cached!.TextureBatches.Values.Single().Single().TextureData);
|
Assert.Equal(textureBytes, cached!.TextureBatches.Values.Single().Single().TextureData);
|
||||||
Assert.True(cache.TryGetAndStage(data.ObjectId, staging, out _, out _));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
data.ObjectId, staging, ownership, out _, out _));
|
||||||
Assert.True(staging.TryDequeue(out var restaged));
|
Assert.True(staging.TryDequeue(out var restaged));
|
||||||
Assert.Same(data, restaged.Data);
|
Assert.Same(data, restaged.Data);
|
||||||
Assert.False(staging.TryDequeue(out _));
|
Assert.False(staging.TryDequeue(out _));
|
||||||
|
|
@ -89,19 +92,22 @@ public sealed class MeshUploadCachesTests
|
||||||
{
|
{
|
||||||
var staging = new MeshUploadStagingQueue(maximumCount: 8, maximumBytes: 64);
|
var staging = new MeshUploadStagingQueue(maximumCount: 8, maximumBytes: 64);
|
||||||
var cache = new CpuMeshUploadCache(capacity: 2);
|
var cache = new CpuMeshUploadCache(capacity: 2);
|
||||||
|
var ownership = Owned(2);
|
||||||
ObjectMeshData queued = DataWithTexture(1, 40);
|
ObjectMeshData queued = DataWithTexture(1, 40);
|
||||||
ObjectMeshData cached = DataWithTexture(2, 40);
|
ObjectMeshData cached = DataWithTexture(2, 40);
|
||||||
cache.Store(cached);
|
cache.Store(cached);
|
||||||
Assert.True(staging.Stage(queued));
|
Assert.True(staging.Stage(queued));
|
||||||
|
|
||||||
Assert.True(cache.TryGetAndStage(2, staging, out ObjectMeshData? found, out MeshStageResult result));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
2, staging, ownership, out ObjectMeshData? found, out MeshStageResult result));
|
||||||
Assert.Same(cached, found);
|
Assert.Same(cached, found);
|
||||||
Assert.Equal(MeshStageResult.HighWater, result);
|
Assert.Equal(MeshStageResult.HighWater, result);
|
||||||
Assert.Equal(1, staging.Count);
|
Assert.Equal(1, staging.Count);
|
||||||
|
|
||||||
Assert.True(staging.TryDequeue(out MeshUploadQueueItem queuedItem));
|
Assert.True(staging.TryDequeue(out MeshUploadQueueItem queuedItem));
|
||||||
staging.Complete(queuedItem);
|
staging.Complete(queuedItem);
|
||||||
Assert.True(cache.TryGetAndStage(2, staging, out _, out result));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
2, staging, ownership, out _, out result));
|
||||||
Assert.Equal(MeshStageResult.Staged, result);
|
Assert.Equal(MeshStageResult.Staged, result);
|
||||||
Assert.True(staging.TryDequeue(out MeshUploadQueueItem staged));
|
Assert.True(staging.TryDequeue(out MeshUploadQueueItem staged));
|
||||||
Assert.Same(cached, staged.Data);
|
Assert.Same(cached, staged.Data);
|
||||||
|
|
@ -142,23 +148,60 @@ public sealed class MeshUploadCachesTests
|
||||||
Assert.Equal(0, staging.Count);
|
Assert.Equal(0, staging.Count);
|
||||||
|
|
||||||
ownership.Acquire(id);
|
ownership.Acquire(id);
|
||||||
Assert.True(cache.TryGetAndStage(id, staging, out ObjectMeshData? rearmed, out _));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
id, staging, ownership, out ObjectMeshData? rearmed, out _));
|
||||||
Assert.Same(data, rearmed);
|
Assert.Same(data, rearmed);
|
||||||
Assert.Equal(0, staging.DiscardUnownedPrefix(ownership, maximum: 1));
|
Assert.Equal(0, staging.DiscardUnownedPrefix(ownership, maximum: 1));
|
||||||
Assert.True(staging.TryPeek(out MeshUploadQueueItem queued));
|
Assert.True(staging.TryPeek(out MeshUploadQueueItem queued));
|
||||||
Assert.Same(data, queued.Data);
|
Assert.Same(data, queued.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CpuCacheHitWithoutOwnerDoesNotRecreateStaleUploadWork()
|
||||||
|
{
|
||||||
|
const ulong id = 0x01000010;
|
||||||
|
var staging = new MeshUploadStagingQueue();
|
||||||
|
var cache = new CpuMeshUploadCache(capacity: 2);
|
||||||
|
var ownership = new MeshOwnershipCounter();
|
||||||
|
ObjectMeshData data = DataWithTexture(id, 16);
|
||||||
|
cache.Store(data);
|
||||||
|
|
||||||
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
id,
|
||||||
|
staging,
|
||||||
|
ownership,
|
||||||
|
out ObjectMeshData? cached,
|
||||||
|
out MeshStageResult result));
|
||||||
|
|
||||||
|
Assert.Same(data, cached);
|
||||||
|
Assert.Equal(MeshStageResult.NotOwned, result);
|
||||||
|
Assert.Equal(0, staging.Count);
|
||||||
|
Assert.Equal(0, staging.ClaimCount);
|
||||||
|
|
||||||
|
ownership.Acquire(id);
|
||||||
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
id,
|
||||||
|
staging,
|
||||||
|
ownership,
|
||||||
|
out _,
|
||||||
|
out result));
|
||||||
|
Assert.Equal(MeshStageResult.Staged, result);
|
||||||
|
Assert.Equal(1, staging.Count);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CpuCacheUsesByteBudgetInAdditionToEntryCount()
|
public void CpuCacheUsesByteBudgetInAdditionToEntryCount()
|
||||||
{
|
{
|
||||||
var staging = new MeshUploadStagingQueue();
|
var staging = new MeshUploadStagingQueue();
|
||||||
var cache = new CpuMeshUploadCache(capacity: 10, byteCapacity: 64);
|
var cache = new CpuMeshUploadCache(capacity: 10, byteCapacity: 64);
|
||||||
|
var ownership = Owned(1, 2);
|
||||||
cache.Store(DataWithTexture(1, 40));
|
cache.Store(DataWithTexture(1, 40));
|
||||||
cache.Store(DataWithTexture(2, 40));
|
cache.Store(DataWithTexture(2, 40));
|
||||||
|
|
||||||
Assert.False(cache.TryGetAndStage(1, staging, out _, out _));
|
Assert.False(cache.TryGetAndStageOwned(
|
||||||
Assert.True(cache.TryGetAndStage(2, staging, out _, out _));
|
1, staging, ownership, out _, out _));
|
||||||
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
2, staging, ownership, out _, out _));
|
||||||
Assert.Equal(1, cache.Count);
|
Assert.Equal(1, cache.Count);
|
||||||
Assert.Equal(40, cache.ResidentBytes);
|
Assert.Equal(40, cache.ResidentBytes);
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +211,7 @@ public sealed class MeshUploadCachesTests
|
||||||
{
|
{
|
||||||
var staging = new MeshUploadStagingQueue(maximumBytes: 256);
|
var staging = new MeshUploadStagingQueue(maximumBytes: 256);
|
||||||
var cache = new CpuMeshUploadCache(capacity: 2, byteCapacity: 64);
|
var cache = new CpuMeshUploadCache(capacity: 2, byteCapacity: 64);
|
||||||
|
var ownership = Owned(1, 2);
|
||||||
ObjectMeshData retained = DataWithTexture(1, 40);
|
ObjectMeshData retained = DataWithTexture(1, 40);
|
||||||
ObjectMeshData oversized = DataWithTexture(2, 80);
|
ObjectMeshData oversized = DataWithTexture(2, 80);
|
||||||
|
|
||||||
|
|
@ -176,8 +220,10 @@ public sealed class MeshUploadCachesTests
|
||||||
|
|
||||||
Assert.Equal(1, cache.Count);
|
Assert.Equal(1, cache.Count);
|
||||||
Assert.Equal(40, cache.ResidentBytes);
|
Assert.Equal(40, cache.ResidentBytes);
|
||||||
Assert.False(cache.TryGetAndStage(2, staging, out _, out _));
|
Assert.False(cache.TryGetAndStageOwned(
|
||||||
Assert.True(cache.TryGetAndStage(1, staging, out ObjectMeshData? found, out _));
|
2, staging, ownership, out _, out _));
|
||||||
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
1, staging, ownership, out ObjectMeshData? found, out _));
|
||||||
Assert.Same(retained, found);
|
Assert.Same(retained, found);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,11 +232,13 @@ public sealed class MeshUploadCachesTests
|
||||||
{
|
{
|
||||||
var staging = new MeshUploadStagingQueue(maximumBytes: 256);
|
var staging = new MeshUploadStagingQueue(maximumBytes: 256);
|
||||||
var cache = new CpuMeshUploadCache(capacity: 2, byteCapacity: 64);
|
var cache = new CpuMeshUploadCache(capacity: 2, byteCapacity: 64);
|
||||||
|
var ownership = Owned(1);
|
||||||
ObjectMeshData retained = DataWithTexture(1, 40);
|
ObjectMeshData retained = DataWithTexture(1, 40);
|
||||||
|
|
||||||
Assert.True(cache.Store(retained));
|
Assert.True(cache.Store(retained));
|
||||||
Assert.False(cache.Store(DataWithTexture(1, 80)));
|
Assert.False(cache.Store(DataWithTexture(1, 80)));
|
||||||
Assert.True(cache.TryGetAndStage(1, staging, out ObjectMeshData? found, out _));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
1, staging, ownership, out ObjectMeshData? found, out _));
|
||||||
Assert.Same(retained, found);
|
Assert.Same(retained, found);
|
||||||
Assert.Equal(40, cache.ResidentBytes);
|
Assert.Equal(40, cache.ResidentBytes);
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +259,8 @@ public sealed class MeshUploadCachesTests
|
||||||
// The new owner cache-hits while the dequeued generation still owns
|
// The new owner cache-hits while the dequeued generation still owns
|
||||||
// staging's id claim, so its direct Stage attempt cannot enqueue yet.
|
// staging's id claim, so its direct Stage attempt cannot enqueue yet.
|
||||||
ownership.Acquire(id);
|
ownership.Acquire(id);
|
||||||
Assert.True(cache.TryGetAndStage(id, staging, out ObjectMeshData? cached, out _));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
id, staging, ownership, out ObjectMeshData? cached, out _));
|
||||||
Assert.Same(data, cached);
|
Assert.Same(data, cached);
|
||||||
Assert.Equal(0, staging.Count);
|
Assert.Equal(0, staging.Count);
|
||||||
|
|
||||||
|
|
@ -237,7 +286,8 @@ public sealed class MeshUploadCachesTests
|
||||||
Assert.Equal(0, staging.Count);
|
Assert.Equal(0, staging.Count);
|
||||||
|
|
||||||
ownership.Acquire(id);
|
ownership.Acquire(id);
|
||||||
Assert.True(cache.TryGetAndStage(id, staging, out ObjectMeshData? cached, out _));
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
id, staging, ownership, out ObjectMeshData? cached, out _));
|
||||||
Assert.Same(data, cached);
|
Assert.Same(data, cached);
|
||||||
Assert.True(staging.TryDequeue(out MeshUploadQueueItem handedOff));
|
Assert.True(staging.TryDequeue(out MeshUploadQueueItem handedOff));
|
||||||
Assert.Same(data, handedOff.Data);
|
Assert.Same(data, handedOff.Data);
|
||||||
|
|
@ -305,10 +355,13 @@ public sealed class MeshUploadCachesTests
|
||||||
// is auditable from an artifact alone.
|
// is auditable from an artifact alone.
|
||||||
var staging = new MeshUploadStagingQueue();
|
var staging = new MeshUploadStagingQueue();
|
||||||
var cache = new CpuMeshUploadCache(capacity: 1);
|
var cache = new CpuMeshUploadCache(capacity: 1);
|
||||||
|
var ownership = Owned(1);
|
||||||
|
|
||||||
Assert.False(cache.TryGetAndStage(1, staging, out _, out _)); // miss
|
Assert.False(cache.TryGetAndStageOwned(
|
||||||
|
1, staging, ownership, out _, out _)); // miss
|
||||||
cache.Store(DataWithTexture(1, 4));
|
cache.Store(DataWithTexture(1, 4));
|
||||||
Assert.True(cache.TryGetAndStage(1, staging, out _, out _)); // hit
|
Assert.True(cache.TryGetAndStageOwned(
|
||||||
|
1, staging, ownership, out _, out _)); // hit
|
||||||
cache.Store(DataWithTexture(2, 4)); // capacity=1 evicts id 1
|
cache.Store(DataWithTexture(2, 4)); // capacity=1 evicts id 1
|
||||||
|
|
||||||
CacheStats stats = cache.Stats;
|
CacheStats stats = cache.Stats;
|
||||||
|
|
@ -317,6 +370,14 @@ public sealed class MeshUploadCachesTests
|
||||||
Assert.Equal(1, stats.Evictions);
|
Assert.Equal(1, stats.Evictions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MeshOwnershipCounter Owned(params ulong[] ids)
|
||||||
|
{
|
||||||
|
var ownership = new MeshOwnershipCounter();
|
||||||
|
foreach (ulong id in ids)
|
||||||
|
ownership.Acquire(id);
|
||||||
|
return ownership;
|
||||||
|
}
|
||||||
|
|
||||||
private static ObjectMeshData DataWithTexture(ulong id, int bytes)
|
private static ObjectMeshData DataWithTexture(ulong id, int bytes)
|
||||||
{
|
{
|
||||||
var data = new ObjectMeshData { ObjectId = id };
|
var data = new ObjectMeshData { ObjectId = id };
|
||||||
|
|
|
||||||
|
|
@ -435,6 +435,43 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
Assert.Equal(0, state.PendingVisibilityTransitionCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void QuiescedDestinationProjection_AcquiresSpatialPresentationBeforeWorldReveal()
|
||||||
|
{
|
||||||
|
const uint landblock = 0x0101FFFFu;
|
||||||
|
const uint cell = 0x01010001u;
|
||||||
|
const uint guid = 0x70000021u;
|
||||||
|
var availability = new WorldGenerationAvailabilityState();
|
||||||
|
availability.Begin(7);
|
||||||
|
var state = new GpuWorldState(availability: availability);
|
||||||
|
state.AddLandblock(new LoadedLandblock(
|
||||||
|
landblock,
|
||||||
|
new LandBlock(),
|
||||||
|
Array.Empty<WorldEntity>()));
|
||||||
|
var runtime = new LiveEntityRuntime(
|
||||||
|
state,
|
||||||
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
||||||
|
WorldSession.EntitySpawn spawn = Spawn(guid, cell);
|
||||||
|
runtime.RegisterLiveEntity(spawn);
|
||||||
|
var edges = new List<bool>();
|
||||||
|
runtime.ProjectionVisibilityChanged += (_, visible) => edges.Add(visible);
|
||||||
|
|
||||||
|
runtime.MaterializeLiveEntity(
|
||||||
|
guid,
|
||||||
|
cell,
|
||||||
|
id => Entity(id, guid));
|
||||||
|
|
||||||
|
Assert.False(state.IsLiveEntityVisible(guid));
|
||||||
|
Assert.True(state.IsLiveEntityProjectionResident(guid));
|
||||||
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
|
Assert.Equal([true], edges);
|
||||||
|
|
||||||
|
Assert.True(availability.End(7));
|
||||||
|
Assert.True(state.IsLiveEntityVisible(guid));
|
||||||
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CopyLiveEntitiesNearLandblock_UsesBoundedNeighborhoodAndSkipsStatics()
|
public void CopyLiveEntitiesNearLandblock_UsesBoundedNeighborhoodAndSkipsStatics()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue