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>
796 lines
33 KiB
C#
796 lines
33 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Wb;
|
|
|
|
public sealed class EntitySpawnAdapterLifetimeTests
|
|
{
|
|
[Fact]
|
|
public void NinetySixLiveOwners_DeleteAndGuidReuse_BalanceEveryMeshReference()
|
|
{
|
|
const int ownerCount = 96;
|
|
const int reuseCount = 32;
|
|
const uint guidBase = 0x74000000u;
|
|
var meshes = new RefCountingMeshAdapter();
|
|
var textures = new RecordingTextureLifetime();
|
|
var adapter = new EntitySpawnAdapter(
|
|
textures,
|
|
_ => MakeSequencer(),
|
|
meshes);
|
|
|
|
for (int i = 0; i < ownerCount; i++)
|
|
{
|
|
uint guid = guidBase + (uint)i;
|
|
WorldEntity entity = MakeEntity((uint)i + 1u, guid);
|
|
entity.MeshRefs =
|
|
[
|
|
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
|
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
|
];
|
|
entity.ApplyAppearance(
|
|
entity.MeshRefs,
|
|
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);
|
|
for (int i = 0; i < reuseCount; i++)
|
|
{
|
|
uint guid = guidBase + (uint)i;
|
|
adapter.OnRemove(guid);
|
|
Assert.Null(adapter.GetState(guid));
|
|
|
|
WorldEntity replacement = MakeEntity(1000u + (uint)i, guid);
|
|
replacement.ApplyAppearance(
|
|
replacement.MeshRefs,
|
|
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++)
|
|
{
|
|
uint guid = guidBase + (uint)i;
|
|
adapter.OnRemove(guid);
|
|
Assert.Null(adapter.GetState(guid));
|
|
}
|
|
|
|
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()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = [new MeshRef(0x01000001u, Matrix4x4.Identity)],
|
|
};
|
|
|
|
private static AnimationSequencer MakeSequencer() =>
|
|
new(new Setup(), new MotionTable(), new NullAnimationLoader());
|
|
|
|
private sealed class NullAnimationLoader : IAnimationLoader
|
|
{
|
|
public Animation? LoadAnimation(uint id) => null;
|
|
}
|
|
|
|
private sealed class RecordingTextureLifetime : IEntityTextureLifetime
|
|
{
|
|
public List<uint> ReleasedOwnerIds { get; } = new();
|
|
|
|
public void ReleaseOwner(uint localEntityId) => ReleasedOwnerIds.Add(localEntityId);
|
|
}
|
|
|
|
private sealed class RefCountingMeshAdapter : IWbMeshAdapter
|
|
{
|
|
public Dictionary<ulong, int> ReferenceCounts { get; } = new();
|
|
public int IncrementCount { get; private set; }
|
|
public int DecrementCount { get; private set; }
|
|
public int TotalReferenceCount => ReferenceCounts.Values.Sum();
|
|
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
ReferenceCounts[id] = ReferenceCounts.GetValueOrDefault(id) + 1;
|
|
IncrementCount++;
|
|
}
|
|
|
|
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;
|
|
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"));
|
|
}
|
|
}
|
|
}
|
|
}
|