fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -34,6 +35,11 @@ public sealed class LiveEntityRuntimeTests
|
|||
public PhysicsBody Body { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class ProjectileRuntime(PhysicsBody body) : ILiveEntityProjectileRuntime
|
||||
{
|
||||
public PhysicsBody Body { get; } = body;
|
||||
}
|
||||
|
||||
private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int RegisterCount { get; private set; }
|
||||
|
|
@ -48,6 +54,34 @@ public sealed class LiveEntityRuntimeTests
|
|||
public void Unregister(WorldEntity entity) => UnregisterCount++;
|
||||
}
|
||||
|
||||
private sealed class FailingRegisterAndRollbackOnceResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
private bool _registerFailurePending = true;
|
||||
private bool _unregisterFailurePending = true;
|
||||
public int RegisterCount { get; private set; }
|
||||
public int UnregisterCount { get; private set; }
|
||||
|
||||
public void Register(WorldEntity entity)
|
||||
{
|
||||
RegisterCount++;
|
||||
if (_registerFailurePending)
|
||||
{
|
||||
_registerFailurePending = false;
|
||||
throw new InvalidOperationException("partial registration failure");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(WorldEntity entity)
|
||||
{
|
||||
UnregisterCount++;
|
||||
if (_unregisterFailurePending)
|
||||
{
|
||||
_unregisterFailurePending = false;
|
||||
throw new InvalidOperationException("rollback failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailingUnregisterResources(uint failingGuid) : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int UnregisterCount { get; private set; }
|
||||
|
|
@ -62,6 +96,38 @@ public sealed class LiveEntityRuntimeTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class FailingOnceUnregisterResources(uint failingGuid) : ILiveEntityResourceLifecycle
|
||||
{
|
||||
private bool _failurePending = true;
|
||||
public int UnregisterCount { get; private set; }
|
||||
public void Register(WorldEntity entity) { }
|
||||
public void Unregister(WorldEntity entity)
|
||||
{
|
||||
UnregisterCount++;
|
||||
if (entity.ServerGuid == failingGuid && _failurePending)
|
||||
{
|
||||
_failurePending = false;
|
||||
throw new InvalidOperationException("fixture one-shot unregister failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CallbackTextureLifetime : IEntityTextureLifetime
|
||||
{
|
||||
public Action? OnRelease { get; set; }
|
||||
public int ReleaseCount { get; private set; }
|
||||
public void ReleaseOwner(uint localEntityId)
|
||||
{
|
||||
ReleaseCount++;
|
||||
OnRelease?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
|
||||
private sealed class CallbackResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public Action<WorldEntity>? OnRegister { get; set; }
|
||||
|
|
@ -123,6 +189,167 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Equal(new[] { false, true }, visibilityEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpatialComponentWorksets_FollowLoadedProjectionWhileLogicalOwnersSurvive()
|
||||
{
|
||||
const uint guid = 0x70000070u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
var animation = new AnimationRuntime(entity);
|
||||
var remote = new RemoteMotionRuntime();
|
||||
var projectile = new ProjectileRuntime(remote.Body);
|
||||
runtime.SetAnimationRuntime(guid, animation);
|
||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||
runtime.SetProjectileRuntime(guid, projectile);
|
||||
|
||||
Assert.Same(animation, Assert.Single(runtime.SpatialAnimationRuntimes).Value);
|
||||
Assert.Same(remote, Assert.Single(runtime.SpatialRemoteMotionRuntimes).Value);
|
||||
Assert.Same(projectile, Assert.Single(runtime.SpatialProjectileRuntimes).Value);
|
||||
|
||||
// Hidden suppresses presentation, not the live CPhysicsObj workset.
|
||||
Assert.True(runtime.TryApplyState(new SetState.Parsed(
|
||||
guid,
|
||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions),
|
||||
1,
|
||||
2), out _));
|
||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
||||
Assert.Single(runtime.SpatialRemoteMotionRuntimes);
|
||||
Assert.Single(runtime.SpatialProjectileRuntimes);
|
||||
|
||||
// A pending projection retains every logical component but disappears
|
||||
// from all per-frame worksets.
|
||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
||||
Assert.Empty(runtime.SpatialAnimationRuntimes);
|
||||
Assert.Empty(runtime.SpatialRemoteMotionRuntimes);
|
||||
Assert.Empty(runtime.SpatialProjectileRuntimes);
|
||||
Assert.True(runtime.TryGetAnimationRuntime(entity.Id, out var retainedAnimation));
|
||||
Assert.Same(animation, retainedAnimation);
|
||||
Assert.True(runtime.TryGetRemoteMotionRuntime(guid, out var retainedRemote));
|
||||
Assert.Same(remote, retainedRemote);
|
||||
Assert.True(runtime.TryGetProjectileRuntime(guid, out var retainedProjectile));
|
||||
Assert.Same(projectile, retainedProjectile);
|
||||
|
||||
spatial.AddLandblock(EmptyLandblock(0x0202FFFFu));
|
||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
||||
Assert.Single(runtime.SpatialRemoteMotionRuntimes);
|
||||
Assert.Single(runtime.SpatialProjectileRuntimes);
|
||||
|
||||
Assert.True(runtime.WithdrawLiveEntityProjection(guid));
|
||||
Assert.Empty(runtime.SpatialAnimationRuntimes);
|
||||
Assert.Empty(runtime.SpatialRemoteMotionRuntimes);
|
||||
Assert.Empty(runtime.SpatialProjectileRuntimes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationView_EnumeratesSpatialOwnersButRetainsLogicalLookupAndRemoval()
|
||||
{
|
||||
const uint guid = 0x70000071u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
var animation = new AnimationRuntime(entity);
|
||||
runtime.SetAnimationRuntime(guid, animation);
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(() => runtime);
|
||||
|
||||
Assert.Equal(1, view.Count);
|
||||
Assert.Equal(entity.Id, Assert.Single(view.Keys));
|
||||
Assert.Same(animation, Assert.Single(view).Value);
|
||||
|
||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
||||
Assert.Equal(0, view.Count);
|
||||
Assert.Empty(view.Keys);
|
||||
Assert.Empty(view);
|
||||
Assert.True(view.TryGetValue(entity.Id, out AnimationRuntime retained));
|
||||
Assert.Same(animation, retained);
|
||||
|
||||
Assert.True(view.Remove(entity.Id));
|
||||
Assert.False(view.TryGetValue(entity.Id, out _));
|
||||
Assert.Empty(runtime.AnimationRuntimes);
|
||||
Assert.Empty(runtime.SpatialAnimationRuntimes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationView_RebucketCallbackSkipsDisplacedSnapshotOwner()
|
||||
{
|
||||
const uint firstGuid = 0x70000073u;
|
||||
const uint secondGuid = 0x70000074u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
foreach (uint guid in new[] { firstGuid, secondGuid })
|
||||
{
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
|
||||
}
|
||||
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(() => runtime);
|
||||
var visited = new List<uint>();
|
||||
|
||||
foreach (KeyValuePair<uint, AnimationRuntime> pair in view)
|
||||
{
|
||||
uint guid = pair.Value.Entity.ServerGuid;
|
||||
visited.Add(guid);
|
||||
uint other = guid == firstGuid ? secondGuid : firstGuid;
|
||||
Assert.True(runtime.RebucketLiveEntity(other, 0x02020001u));
|
||||
}
|
||||
|
||||
Assert.Single(visited);
|
||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
||||
Assert.Equal(2, runtime.AnimationRuntimes.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpatialComponentWorksets_IsolateGuidReuseAcrossVisibilityCallbacks()
|
||||
{
|
||||
const uint guid = 0x70000072u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
WorldEntity oldEntity = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
var oldAnimation = new AnimationRuntime(oldEntity);
|
||||
runtime.SetAnimationRuntime(guid, oldAnimation);
|
||||
|
||||
runtime.ProjectionVisibilityChanged += (record, visible) =>
|
||||
{
|
||||
if (visible || record.Generation != 1)
|
||||
return;
|
||||
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010002u));
|
||||
WorldEntity replacement = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010002u,
|
||||
id => Entity(id, guid))!;
|
||||
runtime.SetAnimationRuntime(guid, new AnimationRuntime(replacement));
|
||||
};
|
||||
|
||||
Assert.False(runtime.WithdrawLiveEntityProjection(guid));
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
||||
Assert.Equal((ushort)2, current.Generation);
|
||||
KeyValuePair<uint, ILiveEntityAnimationRuntime> indexed =
|
||||
Assert.Single(runtime.SpatialAnimationRuntimes);
|
||||
Assert.Same(current.AnimationRuntime, indexed.Value);
|
||||
Assert.NotSame(oldAnimation, indexed.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectProfile_SurvivesRebucketAndClearsWithLogicalTeardown()
|
||||
{
|
||||
|
|
@ -843,6 +1070,100 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Equal(1, resources.UnregisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegistrationRollbackFailureRetainsTombstoneUntilRetryConverges()
|
||||
{
|
||||
const uint guid = 0x7000005Au;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new FailingRegisterAndRollbackOnceResources();
|
||||
var runtime = new LiveEntityRuntime(spatial, resources);
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
|
||||
Assert.Throws<AggregateException>(() => runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid)));
|
||||
|
||||
Assert.Equal(0, runtime.Count);
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
Assert.Equal(1, runtime.MaterializedCount);
|
||||
Assert.False(runtime.TryGetRecord(guid, out _));
|
||||
spatial.MarkPersistent(guid);
|
||||
|
||||
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
||||
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
Assert.Equal(2, resources.UnregisterCount);
|
||||
Assert.Equal(1, spatial.PersistentGuidCount);
|
||||
|
||||
LiveEntityRegistrationResult recovered = runtime.RegisterLiveEntity(
|
||||
Spawn(guid, 1, 1, 0x01010001u));
|
||||
Assert.True(recovered.LogicalRegistrationCreated);
|
||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid));
|
||||
spatial.RemoveLandblock(0x0101FFFFu);
|
||||
|
||||
Assert.Equal(1, spatial.PendingRescueCount);
|
||||
Assert.Equal(1, spatial.PersistentGuidCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegistrationRollbackTombstoneIsIsolatedFromSameGuidReplacement()
|
||||
{
|
||||
const uint guid = 0x7000005Bu;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new FailingRegisterAndRollbackOnceResources();
|
||||
var runtime = new LiveEntityRuntime(spatial, resources);
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
|
||||
Assert.Throws<AggregateException>(() => runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid)));
|
||||
|
||||
runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u));
|
||||
WorldEntity replacement = runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid))!;
|
||||
|
||||
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
||||
Assert.Same(replacement, current.WorldEntity);
|
||||
Assert.Same(replacement, Assert.Single(runtime.MaterializedWorldEntities).Value);
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameGenerationCreateCannotRecoverUntilRegistrationTombstoneConverges()
|
||||
{
|
||||
const uint guid = 0x7000005Cu;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new FailingRegisterAndRollbackOnceResources();
|
||||
var runtime = new LiveEntityRuntime(spatial, resources);
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
Assert.Throws<AggregateException>(() => runtime.MaterializeLiveEntity(
|
||||
guid,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid)));
|
||||
|
||||
LiveEntityRegistrationResult retransmit = runtime.RegisterLiveEntity(spawn);
|
||||
|
||||
Assert.False(retransmit.LogicalRegistrationCreated);
|
||||
Assert.Null(retransmit.Record);
|
||||
Assert.Equal(0, runtime.Count);
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
|
||||
Assert.True(runtime.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(guid, InstanceSequence: 1),
|
||||
isLocalPlayer: false));
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
Assert.False(runtime.TryGetSnapshot(guid, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionClear_AttemptsEveryTeardownAndClearsIdentityWhenOneResourceFails()
|
||||
{
|
||||
|
|
@ -864,11 +1185,107 @@ public sealed class LiveEntityRuntimeTests
|
|||
|
||||
Assert.Equal(2, resources.UnregisterCount);
|
||||
Assert.Equal(0, runtime.Count);
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
Assert.Equal(1, runtime.MaterializedCount);
|
||||
Assert.Empty(runtime.WorldEntities);
|
||||
Assert.Empty(spatial.Entities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_UnregisterFailureRetainsTombstoneAndSameDeleteRetriesCleanup()
|
||||
{
|
||||
const uint guid = 0x7000004Au;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new FailingOnceUnregisterResources(guid);
|
||||
var runtime = new LiveEntityRuntime(spatial, resources);
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid));
|
||||
var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1);
|
||||
|
||||
Assert.Throws<AggregateException>(
|
||||
() => runtime.UnregisterLiveEntity(delete, isLocalPlayer: false));
|
||||
|
||||
Assert.Equal(0, runtime.Count);
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
Assert.Equal(1, runtime.MaterializedCount);
|
||||
Assert.Equal(1, resources.UnregisterCount);
|
||||
|
||||
Assert.True(runtime.UnregisterLiveEntity(delete, isLocalPlayer: false));
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
Assert.Equal(2, resources.UnregisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionClear_OneShotUnregisterFailure_SecondClearConverges()
|
||||
{
|
||||
const uint guid = 0x7000004Bu;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new FailingOnceUnregisterResources(guid);
|
||||
var runtime = new LiveEntityRuntime(spatial, resources);
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid));
|
||||
|
||||
Assert.Throws<AggregateException>(() => runtime.Clear());
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
Assert.Equal(1, runtime.MaterializedCount);
|
||||
|
||||
runtime.Clear();
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
Assert.Empty(spatial.Entities);
|
||||
Assert.Equal(2, resources.UnregisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantDeleteDuringPresentationSuspend_IsQueuedAndNextUpdateCompletesIt()
|
||||
{
|
||||
const uint guid = 0x7000004Cu;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var textures = new CallbackTextureLifetime();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
textures,
|
||||
_ => new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader()));
|
||||
var resources = new DelegateLiveEntityResourceLifecycle(
|
||||
entity => adapter.OnCreate(entity),
|
||||
entity =>
|
||||
{
|
||||
if (!adapter.OnRemove(entity))
|
||||
throw new InvalidOperationException("presentation owner was not registered");
|
||||
});
|
||||
var runtime = new LiveEntityRuntime(spatial, resources);
|
||||
runtime.ProjectionVisibilityChanged += (record, visible) =>
|
||||
{
|
||||
if (record.WorldEntity is { } entity)
|
||||
adapter.SetPresentationResident(entity, visible);
|
||||
};
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
WorldEntity entity = runtime.MaterializeLiveEntity(
|
||||
guid, 0x01010001u, id => Entity(id, guid))!;
|
||||
var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1);
|
||||
textures.OnRelease = () =>
|
||||
{
|
||||
textures.OnRelease = null;
|
||||
runtime.UnregisterLiveEntity(delete, isLocalPlayer: false);
|
||||
};
|
||||
|
||||
Assert.Throws<AggregateException>(
|
||||
() => adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
Assert.NotNull(adapter.GetState(guid));
|
||||
|
||||
Assert.Equal(1, runtime.RetryPendingTeardowns());
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
Assert.Null(adapter.GetState(guid));
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerationReplacement_InstallsNewRecordBeforeReportingOldCleanupFailure()
|
||||
{
|
||||
|
|
@ -1646,13 +2063,19 @@ public sealed class LiveEntityRuntimeTests
|
|||
exception => exception is InvalidOperationException
|
||||
&& exception.Message.Contains("while the session lifetime is clearing", StringComparison.Ordinal));
|
||||
Assert.Equal(0, runtime.Count);
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
Assert.Equal(1, runtime.PendingTeardownCount);
|
||||
Assert.Equal(1, runtime.MaterializedCount);
|
||||
Assert.Empty(runtime.WorldEntities);
|
||||
Assert.Empty(runtime.MaterializedWorldEntities);
|
||||
Assert.Empty(spatial.Entities);
|
||||
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
Assert.Equal(1, resources.UnregisterCount);
|
||||
|
||||
runtime.Clear();
|
||||
Assert.Equal(0, runtime.PendingTeardownCount);
|
||||
Assert.Equal(0, runtime.MaterializedCount);
|
||||
Assert.Empty(runtime.MaterializedWorldEntities);
|
||||
Assert.Equal(2, resources.UnregisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1680,6 +2103,52 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Equal(1, resources.UnregisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncarnationCleanup_OldTombstoneCannotMutateReplacementGuidState()
|
||||
{
|
||||
const uint guid = 0x70000035u;
|
||||
var oldRecord = new LiveEntityRecord(Spawn(guid, 1, 1, 0x01010001u));
|
||||
var replacement = new LiveEntityRecord(Spawn(guid, 2, 1, 0x01010001u));
|
||||
LiveEntityRecord? current = replacement;
|
||||
var cleanup = new LiveEntityIncarnationCleanup(oldRecord, _ => current);
|
||||
object oldHost = new();
|
||||
object newHost = new();
|
||||
var hosts = new Dictionary<uint, object> { [guid] = newHost };
|
||||
bool guidStateCleared = false;
|
||||
|
||||
var plan = new LiveEntityTeardownPlan(
|
||||
[
|
||||
() => cleanup.RunIfNoReplacement(() => guidStateCleared = true),
|
||||
() => cleanup.RemoveCaptured(hosts, oldHost),
|
||||
]);
|
||||
plan.Advance();
|
||||
|
||||
Assert.False(guidStateCleared);
|
||||
Assert.Same(newHost, hosts[guid]);
|
||||
Assert.True(plan.IsComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncarnationCleanup_RemovesOnlyCapturedOwnerAndResumesAfterReplacementEnds()
|
||||
{
|
||||
const uint guid = 0x70000036u;
|
||||
var oldRecord = new LiveEntityRecord(Spawn(guid, 1, 1, 0x01010001u));
|
||||
var replacement = new LiveEntityRecord(Spawn(guid, 2, 1, 0x01010001u));
|
||||
LiveEntityRecord? current = replacement;
|
||||
var cleanup = new LiveEntityIncarnationCleanup(oldRecord, _ => current);
|
||||
object oldHost = new();
|
||||
var hosts = new Dictionary<uint, object> { [guid] = oldHost };
|
||||
int guidCleanupCount = 0;
|
||||
|
||||
cleanup.RemoveCaptured(hosts, oldHost);
|
||||
cleanup.RunIfNoReplacement(() => guidCleanupCount++);
|
||||
current = null;
|
||||
cleanup.RunIfNoReplacement(() => guidCleanupCount++);
|
||||
|
||||
Assert.Empty(hosts);
|
||||
Assert.Equal(1, guidCleanupCount);
|
||||
}
|
||||
|
||||
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
||||
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue