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>
164 lines
5.6 KiB
C#
164 lines
5.6 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Rendering.Wb;
|
|
|
|
public sealed class EntitySpawnAdapterTests
|
|
{
|
|
// ── Happy-path: server-spawned entity ─────────────────────────────────
|
|
|
|
[Fact]
|
|
public void OnCreate_ServerSpawnedEntity_RegistersAnimatedEntityState()
|
|
{
|
|
var lifetime = new RecordingTextureLifetime();
|
|
var adapter = MakeAdapter(lifetime);
|
|
var entity = MakeEntity(id: 1, serverGuid: 0xDEAD0001u);
|
|
|
|
var state = adapter.OnCreate(entity);
|
|
|
|
Assert.NotNull(state);
|
|
Assert.Same(state, adapter.GetState(0xDEAD0001u));
|
|
}
|
|
|
|
[Fact]
|
|
public void OnCreate_ServerSpawnedEntity_SequencerIsNotNull()
|
|
{
|
|
var adapter = MakeAdapter();
|
|
var entity = MakeEntity(id: 1, serverGuid: 0xDEAD0002u);
|
|
|
|
var state = adapter.OnCreate(entity);
|
|
|
|
Assert.NotNull(state!.Sequencer);
|
|
}
|
|
|
|
// ── Atlas-tier filter ─────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void OnCreate_ProceduralEntity_ReturnsNullAndRegistersNothing()
|
|
{
|
|
var lifetime = new RecordingTextureLifetime();
|
|
var adapter = MakeAdapter(lifetime);
|
|
// ServerGuid == 0 → atlas-tier, must not be processed here.
|
|
var entity = MakeEntity(id: 2, serverGuid: 0u);
|
|
|
|
var state = adapter.OnCreate(entity);
|
|
|
|
Assert.Null(state);
|
|
Assert.Null(adapter.GetState(0u));
|
|
Assert.Empty(lifetime.ReleasedOwnerIds);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnCreate_DoesNotAcquireTexturesBeforeTheSurfaceIsDrawn()
|
|
{
|
|
var lifetime = new RecordingTextureLifetime();
|
|
var adapter = MakeAdapter(lifetime);
|
|
var entity = MakeEntity(id: 10, serverGuid: 0xBEEF0001u);
|
|
|
|
adapter.OnCreate(entity);
|
|
|
|
Assert.Empty(lifetime.ReleasedOwnerIds);
|
|
}
|
|
|
|
// ── OnRemove ─────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void OnRemove_ReleasesPerEntityState()
|
|
{
|
|
var lifetime = new RecordingTextureLifetime();
|
|
var adapter = MakeAdapter(lifetime);
|
|
var entity = MakeEntity(id: 20, serverGuid: 0xCAFE0001u);
|
|
|
|
adapter.OnCreate(entity);
|
|
adapter.SetPresentationResident(entity, resident: true);
|
|
Assert.NotNull(adapter.GetState(0xCAFE0001u));
|
|
|
|
adapter.OnRemove(0xCAFE0001u);
|
|
Assert.Null(adapter.GetState(0xCAFE0001u));
|
|
Assert.Equal([20u], lifetime.ReleasedOwnerIds);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnRemove_UnknownGuid_NoOps()
|
|
{
|
|
var adapter = MakeAdapter();
|
|
|
|
// Must not throw.
|
|
adapter.OnRemove(0xDEADBEEFu);
|
|
}
|
|
|
|
// ── Multiple entities ─────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void OnCreate_MultipleEntities_EachGetsOwnState()
|
|
{
|
|
var adapter = MakeAdapter();
|
|
var e1 = MakeEntity(id: 30, serverGuid: 0x11110001u);
|
|
var e2 = MakeEntity(id: 31, serverGuid: 0x11110002u);
|
|
|
|
var s1 = adapter.OnCreate(e1);
|
|
var s2 = adapter.OnCreate(e2);
|
|
|
|
Assert.NotNull(s1);
|
|
Assert.NotNull(s2);
|
|
Assert.NotSame(s1, s2);
|
|
Assert.Same(s1, adapter.GetState(0x11110001u));
|
|
Assert.Same(s2, adapter.GetState(0x11110002u));
|
|
}
|
|
|
|
[Fact]
|
|
public void OnRemove_OnlyReleasesTargetGuid()
|
|
{
|
|
var adapter = MakeAdapter();
|
|
var e1 = MakeEntity(id: 40, serverGuid: 0x22220001u);
|
|
var e2 = MakeEntity(id: 41, serverGuid: 0x22220002u);
|
|
|
|
adapter.OnCreate(e1);
|
|
adapter.OnCreate(e2);
|
|
adapter.OnRemove(0x22220001u);
|
|
|
|
Assert.Null(adapter.GetState(0x22220001u));
|
|
Assert.NotNull(adapter.GetState(0x22220002u));
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
private static EntitySpawnAdapter MakeAdapter(IEntityTextureLifetime? lifetime = null)
|
|
{
|
|
lifetime ??= new RecordingTextureLifetime();
|
|
return new EntitySpawnAdapter(lifetime, _ => MakeSequencer());
|
|
}
|
|
|
|
private static WorldEntity MakeEntity(uint id, uint serverGuid)
|
|
=> new WorldEntity
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = new[] { new MeshRef(0x01000001u, Matrix4x4.Identity) },
|
|
};
|
|
|
|
private static AnimationSequencer MakeSequencer()
|
|
=> new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
|
|
|
|
// ── Mocks / stubs ─────────────────────────────────────────────────────
|
|
|
|
private sealed class RecordingTextureLifetime : IEntityTextureLifetime
|
|
{
|
|
public List<uint> ReleasedOwnerIds { get; } = new();
|
|
|
|
public void ReleaseOwner(uint localEntityId) => ReleasedOwnerIds.Add(localEntityId);
|
|
}
|
|
|
|
private sealed class NullAnimationLoader : IAnimationLoader
|
|
{
|
|
public Animation? LoadAnimation(uint id) => null;
|
|
}
|
|
}
|