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
|
|
@ -15,8 +15,9 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
|||
const int reuseCount = 32;
|
||||
const uint guidBase = 0x74000000u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var textures = new RecordingTextureLifetime();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new NullTextureCache(),
|
||||
textures,
|
||||
_ => MakeSequencer(),
|
||||
meshes);
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
|||
entity.PaletteOverride,
|
||||
[new PartOverride(0, 0x01001000u + (uint)i)]);
|
||||
Assert.NotNull(adapter.OnCreate(entity));
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
}
|
||||
|
||||
Assert.Equal(ownerCount * 2, meshes.TotalReferenceCount);
|
||||
|
|
@ -49,6 +51,7 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
|||
replacement.PaletteOverride,
|
||||
[new PartOverride(0, 0x01002000u + (uint)i)]);
|
||||
Assert.NotNull(adapter.OnCreate(replacement));
|
||||
Assert.True(adapter.SetPresentationResident(replacement, resident: true));
|
||||
}
|
||||
|
||||
for (int i = 0; i < ownerCount; i++)
|
||||
|
|
@ -61,6 +64,570 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
|||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Empty(meshes.ReferenceCounts);
|
||||
Assert.Equal(meshes.IncrementCount, meshes.DecrementCount);
|
||||
Assert.Equal(ownerCount + reuseCount, textures.ReleasedOwnerIds.Count);
|
||||
Assert.Equal(ownerCount + reuseCount, textures.ReleasedOwnerIds.Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectionSuspendResume_IsIdempotentAndPreservesAnimatedState()
|
||||
{
|
||||
const uint guid = 0x74001000u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var textures = new RecordingTextureLifetime();
|
||||
int sequencerCreates = 0;
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
textures,
|
||||
_ =>
|
||||
{
|
||||
sequencerCreates++;
|
||||
return MakeSequencer();
|
||||
},
|
||||
meshes);
|
||||
WorldEntity entity = MakeEntity(41u, guid);
|
||||
entity.ApplyAppearance(
|
||||
[
|
||||
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
||||
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
||||
],
|
||||
entity.PaletteOverride,
|
||||
[new PartOverride(0, 0x01000002u)]);
|
||||
|
||||
AnimatedEntityState state = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(entity));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.False(adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.Empty(textures.ReleasedOwnerIds);
|
||||
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.False(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.Equal(2, meshes.TotalReferenceCount);
|
||||
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.False(adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal([entity.Id], textures.ReleasedOwnerIds);
|
||||
Assert.Same(state, adapter.GetState(guid));
|
||||
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.Equal(2, meshes.TotalReferenceCount);
|
||||
Assert.Same(state, adapter.GetState(guid));
|
||||
Assert.Equal(1, sequencerCreates);
|
||||
|
||||
adapter.OnRemove(guid);
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal([entity.Id, entity.Id], textures.ReleasedOwnerIds);
|
||||
Assert.Equal(meshes.IncrementCount, meshes.DecrementCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveWhileSuspended_DoesNotReleasePresentationResourcesTwice()
|
||||
{
|
||||
const uint guid = 0x74002000u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var textures = new RecordingTextureLifetime();
|
||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(51u, guid);
|
||||
|
||||
adapter.OnCreate(entity);
|
||||
adapter.SetPresentationResident(entity, resident: true);
|
||||
adapter.SetPresentationResident(entity, resident: false);
|
||||
int decrementsAfterSuspend = meshes.DecrementCount;
|
||||
|
||||
adapter.OnRemove(guid);
|
||||
adapter.OnRemove(guid);
|
||||
|
||||
Assert.Null(adapter.GetState(guid));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(decrementsAfterSuspend, meshes.DecrementCount);
|
||||
Assert.Equal([entity.Id], textures.ReleasedOwnerIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuidReplacement_IgnoresDelayedEdgesFromDisplacedOwner()
|
||||
{
|
||||
const uint guid = 0x74003000u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var textures = new RecordingTextureLifetime();
|
||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
WorldEntity oldEntity = MakeEntity(61u, guid);
|
||||
oldEntity.MeshRefs = [new MeshRef(0x01000100u, Matrix4x4.Identity)];
|
||||
WorldEntity replacement = MakeEntity(62u, guid);
|
||||
replacement.MeshRefs = [new MeshRef(0x01000200u, Matrix4x4.Identity)];
|
||||
|
||||
adapter.OnCreate(oldEntity);
|
||||
adapter.SetPresentationResident(oldEntity, resident: true);
|
||||
AnimatedEntityState replacementState = Assert.IsType<AnimatedEntityState>(
|
||||
adapter.OnCreate(replacement));
|
||||
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal([oldEntity.Id], textures.ReleasedOwnerIds);
|
||||
Assert.False(adapter.OnRemove(oldEntity));
|
||||
Assert.False(adapter.SetPresentationResident(oldEntity, resident: false));
|
||||
Assert.False(adapter.SetPresentationResident(oldEntity, resident: true));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
|
||||
Assert.True(adapter.SetPresentationResident(replacement, resident: true));
|
||||
Assert.Equal(1, meshes.TotalReferenceCount);
|
||||
Assert.Same(replacementState, adapter.GetState(guid));
|
||||
|
||||
adapter.OnRemove(guid);
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal([oldEntity.Id, replacement.Id], textures.ReleasedOwnerIds);
|
||||
Assert.Equal(meshes.IncrementCount, meshes.DecrementCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuidReplacement_FactoryFailure_PreservesResidentPriorOwner()
|
||||
{
|
||||
const uint guid = 0x74004000u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var textures = new RecordingTextureLifetime();
|
||||
WorldEntity oldEntity = MakeEntity(71u, guid);
|
||||
oldEntity.MeshRefs = [new MeshRef(0x01000300u, Matrix4x4.Identity)];
|
||||
WorldEntity replacement = MakeEntity(72u, guid);
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
textures,
|
||||
entity => ReferenceEquals(entity, replacement)
|
||||
? throw new InvalidOperationException("replacement factory failure")
|
||||
: MakeSequencer(),
|
||||
meshes);
|
||||
|
||||
AnimatedEntityState oldState = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(oldEntity));
|
||||
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => adapter.OnCreate(replacement));
|
||||
|
||||
Assert.Same(oldState, adapter.GetState(guid));
|
||||
Assert.Equal(1, meshes.TotalReferenceCount);
|
||||
Assert.Empty(textures.ReleasedOwnerIds);
|
||||
Assert.False(adapter.SetPresentationResident(replacement, resident: true));
|
||||
Assert.True(adapter.SetPresentationResident(oldEntity, resident: false));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal([oldEntity.Id], textures.ReleasedOwnerIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectionSuspend_PartialReleaseFailure_RemainsResidentAndRetriesOnlyUnfinishedResources()
|
||||
{
|
||||
const uint guid = 0x74005000u;
|
||||
const ulong firstMesh = 0x01000400u;
|
||||
const ulong secondMesh = 0x01000401u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var textures = new FaultInjectingTextureLifetime();
|
||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(81u, guid);
|
||||
entity.MeshRefs =
|
||||
[
|
||||
new MeshRef((uint)firstMesh, Matrix4x4.Identity),
|
||||
new MeshRef((uint)secondMesh, Matrix4x4.Identity),
|
||||
];
|
||||
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
meshes.FailNextDecrement(secondMesh);
|
||||
|
||||
Assert.Throws<AggregateException>(
|
||||
() => adapter.SetPresentationResident(entity, resident: false));
|
||||
|
||||
Assert.NotNull(adapter.GetState(guid));
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.Equal(2, meshes.TotalReferenceCount);
|
||||
Assert.Equal(1, textures.AttemptCount);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[firstMesh]);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[secondMesh]);
|
||||
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(2, textures.AttemptCount);
|
||||
Assert.Equal(2, meshes.DecrementAttempts[firstMesh]);
|
||||
Assert.Equal(2, meshes.DecrementAttempts[secondMesh]);
|
||||
Assert.False(adapter.SetPresentationResident(entity, resident: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogicalRemove_ReleaseFailure_KeepsOwnerReachableForRetry()
|
||||
{
|
||||
const uint guid = 0x74006000u;
|
||||
const ulong meshId = 0x01000500u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var textures = new FaultInjectingTextureLifetime(failuresBeforeSuccess: 1);
|
||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(91u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)meshId, Matrix4x4.Identity)];
|
||||
AnimatedEntityState state = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(entity));
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
|
||||
Assert.Throws<AggregateException>(() => adapter.OnRemove(entity));
|
||||
|
||||
Assert.Same(state, adapter.GetState(guid));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[meshId]);
|
||||
Assert.Equal(1, textures.AttemptCount);
|
||||
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
Assert.Null(adapter.GetState(guid));
|
||||
Assert.Equal(1, meshes.DecrementAttempts[meshId]);
|
||||
Assert.Equal(2, textures.AttemptCount);
|
||||
Assert.Equal(1, textures.SuccessCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuidReplacement_ReleaseFailure_PublishesReplacementOnlyAfterRetrySucceeds()
|
||||
{
|
||||
const uint guid = 0x74007000u;
|
||||
const ulong oldMeshId = 0x01000600u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var textures = new FaultInjectingTextureLifetime(failuresBeforeSuccess: 1);
|
||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
WorldEntity oldEntity = MakeEntity(101u, guid);
|
||||
oldEntity.MeshRefs = [new MeshRef((uint)oldMeshId, Matrix4x4.Identity)];
|
||||
WorldEntity replacement = MakeEntity(102u, guid);
|
||||
AnimatedEntityState oldState = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(oldEntity));
|
||||
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
||||
|
||||
Assert.Throws<AggregateException>(() => adapter.OnCreate(replacement));
|
||||
|
||||
Assert.Same(oldState, adapter.GetState(guid));
|
||||
Assert.False(adapter.SetPresentationResident(replacement, resident: true));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[oldMeshId]);
|
||||
|
||||
AnimatedEntityState replacementState = Assert.IsType<AnimatedEntityState>(
|
||||
adapter.OnCreate(replacement));
|
||||
Assert.Same(replacementState, adapter.GetState(guid));
|
||||
Assert.False(adapter.OnRemove(oldEntity));
|
||||
Assert.Equal(1, meshes.DecrementAttempts[oldMeshId]);
|
||||
Assert.Equal(2, textures.AttemptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogicalRemove_ReentrantDuplicate_DoesNotPublishRemovalBeforeCleanupCompletes()
|
||||
{
|
||||
const uint guid = 0x74008000u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
EntitySpawnAdapter? adapter = null;
|
||||
WorldEntity entity = MakeEntity(111u, guid);
|
||||
bool invokeNestedRemove = true;
|
||||
var textures = new FaultInjectingTextureLifetime(
|
||||
onRelease: () =>
|
||||
{
|
||||
if (!invokeNestedRemove)
|
||||
return;
|
||||
invokeNestedRemove = false;
|
||||
Assert.NotNull(adapter!.GetState(guid));
|
||||
adapter.OnRemove(entity);
|
||||
});
|
||||
adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
|
||||
AggregateException error = Assert.Throws<AggregateException>(() => adapter.OnRemove(entity));
|
||||
Assert.Contains(
|
||||
error.Flatten().InnerExceptions,
|
||||
exception => exception is EntityPresentationRemovalDeferredException);
|
||||
Assert.NotNull(adapter.GetState(guid));
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
|
||||
Assert.Null(adapter.GetState(guid));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(2, textures.AttemptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogicalRemove_AfterResumeRollbackFailure_ReleasesResidualMeshReference()
|
||||
{
|
||||
const uint guid = 0x74009000u;
|
||||
const ulong firstMesh = 0x01000700u;
|
||||
const ulong secondMesh = 0x01000701u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var textures = new FaultInjectingTextureLifetime();
|
||||
var adapter = new EntitySpawnAdapter(textures, _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(121u, guid);
|
||||
entity.MeshRefs =
|
||||
[
|
||||
new MeshRef((uint)firstMesh, Matrix4x4.Identity),
|
||||
new MeshRef((uint)secondMesh, Matrix4x4.Identity),
|
||||
];
|
||||
AnimatedEntityState state = Assert.IsType<AnimatedEntityState>(adapter.OnCreate(entity));
|
||||
meshes.FailNextIncrement(secondMesh);
|
||||
meshes.FailNextDecrement(firstMesh);
|
||||
|
||||
Assert.Throws<AggregateException>(
|
||||
() => adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.Equal(1, meshes.TotalReferenceCount);
|
||||
Assert.Same(state, adapter.GetState(guid));
|
||||
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
|
||||
Assert.Null(adapter.GetState(guid));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(2, meshes.DecrementAttempts[firstMesh]);
|
||||
Assert.Equal(0, textures.AttemptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resume_IncrementThrowsAfterCommit_RollsBackCommittedReference()
|
||||
{
|
||||
const uint guid = 0x7400A000u;
|
||||
const ulong meshId = 0x01000800u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new FaultInjectingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(131u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)meshId, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
meshes.ThrowAfterNextIncrement(meshId);
|
||||
|
||||
Assert.Throws<MeshReferenceMutationException>(
|
||||
() => adapter.SetPresentationResident(entity, resident: true));
|
||||
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[meshId]);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.Equal(1, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Suspend_DecrementThrowsAfterCommit_RetryDoesNotDoubleDecrement()
|
||||
{
|
||||
const uint guid = 0x7400B000u;
|
||||
const ulong meshId = 0x01000900u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new FaultInjectingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(141u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)meshId, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
meshes.ThrowAfterNextDecrement(meshId);
|
||||
|
||||
Assert.Throws<AggregateException>(
|
||||
() => adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[meshId]);
|
||||
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: false));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.Equal(1, meshes.DecrementAttempts[meshId]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceChange_Resident_AcquiresReplacementBeforePublicationAndRetiresOldAfter()
|
||||
{
|
||||
const uint guid = 0x7400C000u;
|
||||
const ulong oldMesh = 0x01000A00u;
|
||||
const ulong newMesh = 0x01000A01u;
|
||||
const ulong overrideMesh = 0x01000A02u;
|
||||
var meshes = new CallbackMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(151u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
meshes.Operations.Clear();
|
||||
|
||||
MeshRef[] replacement = [new MeshRef((uint)newMesh, Matrix4x4.Identity)];
|
||||
PartOverride[] overrides = [new PartOverride(0, (uint)overrideMesh)];
|
||||
Assert.True(adapter.OnAppearanceChanged(
|
||||
entity,
|
||||
replacement,
|
||||
overrides,
|
||||
() =>
|
||||
{
|
||||
Assert.Equal(1, meshes.ReferenceCounts[oldMesh]);
|
||||
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
|
||||
Assert.Equal(1, meshes.ReferenceCounts[overrideMesh]);
|
||||
meshes.Operations.Add("publish");
|
||||
entity.ApplyAppearance(replacement, paletteOverride: null, overrides);
|
||||
}));
|
||||
|
||||
int publicationIndex = meshes.Operations.IndexOf("publish");
|
||||
Assert.True(meshes.Operations.IndexOf($"increment:{newMesh:X8}") < publicationIndex);
|
||||
Assert.True(meshes.Operations.IndexOf($"increment:{overrideMesh:X8}") < publicationIndex);
|
||||
Assert.True(meshes.Operations.IndexOf($"decrement:{oldMesh:X8}") > publicationIndex);
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(oldMesh));
|
||||
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
|
||||
Assert.Equal(1, meshes.ReferenceCounts[overrideMesh]);
|
||||
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceChange_WhileSuspended_OnlyReplacementSetIsAcquiredOnResume()
|
||||
{
|
||||
const uint guid = 0x7400D000u;
|
||||
const ulong oldMesh = 0x01000B00u;
|
||||
const ulong newMesh = 0x01000B01u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(161u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
|
||||
MeshRef[] replacement = [new MeshRef((uint)newMesh, Matrix4x4.Identity)];
|
||||
Assert.True(adapter.OnAppearanceChanged(
|
||||
entity,
|
||||
replacement,
|
||||
[],
|
||||
() => entity.ApplyAppearance(replacement, paletteOverride: null, [])));
|
||||
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(oldMesh));
|
||||
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceChange_CommittedAcquireFailure_RollsBackAndDoesNotPublish()
|
||||
{
|
||||
const uint guid = 0x7400E000u;
|
||||
const ulong oldMesh = 0x01000C00u;
|
||||
const ulong newMesh = 0x01000C01u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(171u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
meshes.ThrowAfterNextIncrement(newMesh);
|
||||
bool published = false;
|
||||
|
||||
Assert.Throws<MeshReferenceMutationException>(() =>
|
||||
adapter.OnAppearanceChanged(
|
||||
entity,
|
||||
[new MeshRef((uint)newMesh, Matrix4x4.Identity)],
|
||||
[],
|
||||
() => published = true));
|
||||
|
||||
Assert.False(published);
|
||||
Assert.Equal(1, meshes.ReferenceCounts[oldMesh]);
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(newMesh));
|
||||
Assert.Equal(1, meshes.DecrementAttempts[newMesh]);
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceChange_ReleaseFailure_RetainsMarkerForResidencyRetry()
|
||||
{
|
||||
const uint guid = 0x7400F000u;
|
||||
const ulong oldMesh = 0x01000D00u;
|
||||
const ulong newMesh = 0x01000D01u;
|
||||
var meshes = new FaultInjectingMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(181u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
meshes.FailNextDecrement(oldMesh);
|
||||
int publications = 0;
|
||||
MeshRef[] replacement = [new MeshRef((uint)newMesh, Matrix4x4.Identity)];
|
||||
|
||||
Assert.Throws<AggregateException>(() =>
|
||||
adapter.OnAppearanceChanged(
|
||||
entity,
|
||||
replacement,
|
||||
[],
|
||||
() =>
|
||||
{
|
||||
publications++;
|
||||
entity.ApplyAppearance(replacement, paletteOverride: null, []);
|
||||
}));
|
||||
|
||||
Assert.Equal(1, publications);
|
||||
Assert.Equal(2, meshes.TotalReferenceCount);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
Assert.Equal(1, publications);
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(oldMesh));
|
||||
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
|
||||
Assert.Equal(2, meshes.DecrementAttempts[oldMesh]);
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceChange_ReentrantRequestCannotPublishInsideOuterTransition()
|
||||
{
|
||||
const uint guid = 0x74010000u;
|
||||
const ulong oldMesh = 0x01000E00u;
|
||||
const ulong outerMesh = 0x01000E01u;
|
||||
const ulong nestedMesh = 0x01000E02u;
|
||||
var meshes = new CallbackMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity entity = MakeEntity(191u, guid);
|
||||
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(entity);
|
||||
Assert.True(adapter.SetPresentationResident(entity, resident: true));
|
||||
bool nestedPublished = false;
|
||||
bool nestedResult = true;
|
||||
meshes.AfterIncrement = meshId =>
|
||||
{
|
||||
if (meshId != outerMesh)
|
||||
return;
|
||||
|
||||
nestedResult = adapter.OnAppearanceChanged(
|
||||
entity,
|
||||
[new MeshRef((uint)nestedMesh, Matrix4x4.Identity)],
|
||||
[],
|
||||
() => nestedPublished = true);
|
||||
};
|
||||
|
||||
MeshRef[] replacement = [new MeshRef((uint)outerMesh, Matrix4x4.Identity)];
|
||||
Assert.True(adapter.OnAppearanceChanged(
|
||||
entity,
|
||||
replacement,
|
||||
[],
|
||||
() => entity.ApplyAppearance(replacement, paletteOverride: null, [])));
|
||||
|
||||
Assert.False(nestedResult);
|
||||
Assert.False(nestedPublished);
|
||||
Assert.Equal(1, meshes.ReferenceCounts[outerMesh]);
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(oldMesh));
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(nestedMesh));
|
||||
Assert.True(adapter.OnRemove(entity));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceChange_DelayedOldIncarnationCannotAffectGuidReplacement()
|
||||
{
|
||||
const uint guid = 0x74011000u;
|
||||
const ulong oldMesh = 0x01000F00u;
|
||||
const ulong replacementMesh = 0x01000F01u;
|
||||
const ulong staleMesh = 0x01000F02u;
|
||||
var meshes = new RefCountingMeshAdapter();
|
||||
var adapter = new EntitySpawnAdapter(
|
||||
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
|
||||
WorldEntity oldEntity = MakeEntity(201u, guid);
|
||||
oldEntity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(oldEntity);
|
||||
Assert.True(adapter.SetPresentationResident(oldEntity, resident: true));
|
||||
|
||||
WorldEntity replacement = MakeEntity(202u, guid);
|
||||
replacement.MeshRefs = [new MeshRef((uint)replacementMesh, Matrix4x4.Identity)];
|
||||
adapter.OnCreate(replacement);
|
||||
Assert.True(adapter.SetPresentationResident(replacement, resident: true));
|
||||
bool stalePublished = false;
|
||||
|
||||
Assert.False(adapter.OnAppearanceChanged(
|
||||
oldEntity,
|
||||
[new MeshRef((uint)staleMesh, Matrix4x4.Identity)],
|
||||
[],
|
||||
() => stalePublished = true));
|
||||
|
||||
Assert.False(stalePublished);
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(oldMesh));
|
||||
Assert.False(meshes.ReferenceCounts.ContainsKey(staleMesh));
|
||||
Assert.Equal(1, meshes.ReferenceCounts[replacementMesh]);
|
||||
Assert.True(adapter.OnRemove(replacement));
|
||||
Assert.Equal(0, meshes.TotalReferenceCount);
|
||||
}
|
||||
|
||||
private static WorldEntity MakeEntity(uint id, uint serverGuid) => new()
|
||||
|
|
@ -81,12 +648,11 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
|||
public Animation? LoadAnimation(uint id) => null;
|
||||
}
|
||||
|
||||
private sealed class NullTextureCache : ITextureCachePerInstance
|
||||
private sealed class RecordingTextureLifetime : IEntityTextureLifetime
|
||||
{
|
||||
public uint GetOrUploadWithPaletteOverride(
|
||||
uint surfaceId,
|
||||
uint? overrideOrigTextureId,
|
||||
PaletteOverride paletteOverride) => 1u;
|
||||
public List<uint> ReleasedOwnerIds { get; } = new();
|
||||
|
||||
public void ReleaseOwner(uint localEntityId) => ReleasedOwnerIds.Add(localEntityId);
|
||||
}
|
||||
|
||||
private sealed class RefCountingMeshAdapter : IWbMeshAdapter
|
||||
|
|
@ -113,4 +679,118 @@ public sealed class EntitySpawnAdapterLifetimeTests
|
|||
DecrementCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CallbackMeshAdapter : IWbMeshAdapter
|
||||
{
|
||||
public Dictionary<ulong, int> ReferenceCounts { get; } = new();
|
||||
public List<string> Operations { get; } = new();
|
||||
public Action<ulong>? AfterIncrement { get; set; }
|
||||
public int TotalReferenceCount => ReferenceCounts.Values.Sum();
|
||||
|
||||
public void IncrementRefCount(ulong id)
|
||||
{
|
||||
ReferenceCounts[id] = ReferenceCounts.GetValueOrDefault(id) + 1;
|
||||
Operations.Add($"increment:{id:X8}");
|
||||
AfterIncrement?.Invoke(id);
|
||||
}
|
||||
|
||||
public void DecrementRefCount(ulong id)
|
||||
{
|
||||
Assert.True(ReferenceCounts.TryGetValue(id, out int count));
|
||||
Assert.True(count > 0);
|
||||
if (count == 1)
|
||||
ReferenceCounts.Remove(id);
|
||||
else
|
||||
ReferenceCounts[id] = count - 1;
|
||||
Operations.Add($"decrement:{id:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FaultInjectingTextureLifetime(
|
||||
int failuresBeforeSuccess = 0,
|
||||
Action? onRelease = null) : IEntityTextureLifetime
|
||||
{
|
||||
private int _failuresRemaining = failuresBeforeSuccess;
|
||||
|
||||
public int AttemptCount { get; private set; }
|
||||
public int SuccessCount { get; private set; }
|
||||
|
||||
public void ReleaseOwner(uint localEntityId)
|
||||
{
|
||||
AttemptCount++;
|
||||
onRelease?.Invoke();
|
||||
if (_failuresRemaining > 0)
|
||||
{
|
||||
_failuresRemaining--;
|
||||
throw new InvalidOperationException("injected texture release failure");
|
||||
}
|
||||
|
||||
SuccessCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FaultInjectingMeshAdapter : IWbMeshAdapter
|
||||
{
|
||||
private readonly Dictionary<ulong, int> _remainingIncrementFailures = new();
|
||||
private readonly Dictionary<ulong, int> _remainingDecrementFailures = new();
|
||||
private readonly HashSet<ulong> _throwAfterIncrement = new();
|
||||
private readonly HashSet<ulong> _throwAfterDecrement = new();
|
||||
|
||||
public Dictionary<ulong, int> ReferenceCounts { get; } = new();
|
||||
public Dictionary<ulong, int> DecrementAttempts { get; } = new();
|
||||
public int TotalReferenceCount => ReferenceCounts.Values.Sum();
|
||||
|
||||
public void FailNextIncrement(ulong id) =>
|
||||
_remainingIncrementFailures[id] =
|
||||
_remainingIncrementFailures.GetValueOrDefault(id) + 1;
|
||||
|
||||
public void FailNextDecrement(ulong id) =>
|
||||
_remainingDecrementFailures[id] =
|
||||
_remainingDecrementFailures.GetValueOrDefault(id) + 1;
|
||||
|
||||
public void ThrowAfterNextIncrement(ulong id) => _throwAfterIncrement.Add(id);
|
||||
public void ThrowAfterNextDecrement(ulong id) => _throwAfterDecrement.Add(id);
|
||||
|
||||
public void IncrementRefCount(ulong id)
|
||||
{
|
||||
if (_remainingIncrementFailures.GetValueOrDefault(id) > 0)
|
||||
{
|
||||
_remainingIncrementFailures[id]--;
|
||||
throw new InvalidOperationException("injected mesh acquire failure");
|
||||
}
|
||||
|
||||
ReferenceCounts[id] = ReferenceCounts.GetValueOrDefault(id) + 1;
|
||||
if (_throwAfterIncrement.Remove(id))
|
||||
{
|
||||
throw new MeshReferenceMutationException(
|
||||
"injected post-commit mesh acquire failure",
|
||||
mutationCommitted: true,
|
||||
new InvalidOperationException("injected metadata failure"));
|
||||
}
|
||||
}
|
||||
|
||||
public void DecrementRefCount(ulong id)
|
||||
{
|
||||
DecrementAttempts[id] = DecrementAttempts.GetValueOrDefault(id) + 1;
|
||||
if (_remainingDecrementFailures.GetValueOrDefault(id) > 0)
|
||||
{
|
||||
_remainingDecrementFailures[id]--;
|
||||
throw new InvalidOperationException("injected mesh release failure");
|
||||
}
|
||||
|
||||
Assert.True(ReferenceCounts.TryGetValue(id, out int count));
|
||||
Assert.True(count > 0);
|
||||
if (count == 1)
|
||||
ReferenceCounts.Remove(id);
|
||||
else
|
||||
ReferenceCounts[id] = count - 1;
|
||||
if (_throwAfterDecrement.Remove(id))
|
||||
{
|
||||
throw new MeshReferenceMutationException(
|
||||
"injected post-commit mesh release failure",
|
||||
mutationCommitted: true,
|
||||
new InvalidOperationException("injected cancellation failure"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue