fix(portal): synchronize destination presentation state

This commit is contained in:
Erik 2026-07-16 21:17:13 +02:00
parent 4b1bceefbb
commit e95f55f25b
42 changed files with 2815 additions and 288 deletions

View file

@ -0,0 +1,104 @@
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class LandblockSpawnAdapterReadinessTests
{
[Fact]
public void RenderReady_WaitsForStaticAndEnvCellMeshes()
{
var meshes = new ReadinessMeshAdapter();
var adapter = new LandblockSpawnAdapter(meshes);
var entity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = 0x01000010u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [new MeshRef(0x01000010u, Matrix4x4.Identity)],
};
var landblock = new LoadedLandblock(
0x1234FFFFu,
new LandBlock(),
new[] { entity });
const ulong envCellGeometryId = 0x2_0000_1234ul;
adapter.OnLandblockLoaded(landblock, new[] { envCellGeometryId });
Assert.False(adapter.IsLandblockRenderReady(landblock.LandblockId));
meshes.ReadyIds.Add(0x01000010ul);
Assert.False(adapter.IsLandblockRenderReady(landblock.LandblockId));
meshes.ReadyIds.Add(envCellGeometryId);
Assert.True(adapter.IsLandblockRenderReady(landblock.LandblockId));
}
[Fact]
public void RenderReady_EmptyPublishedLandblockIsReadyUntilUnload()
{
var meshes = new ReadinessMeshAdapter();
var adapter = new LandblockSpawnAdapter(meshes);
var landblock = new LoadedLandblock(
0x1234FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>());
adapter.OnLandblockLoaded(landblock);
Assert.True(adapter.IsLandblockRenderReady(landblock.LandblockId));
adapter.OnLandblockUnloaded(landblock.LandblockId);
Assert.False(adapter.IsLandblockRenderReady(landblock.LandblockId));
}
[Fact]
public void SharedEnvCellGeometry_IsPinnedPerLandblockAndReleasedSymmetrically()
{
const ulong sharedGeometryId = 0x2_0000_1234ul;
var meshes = new ReadinessMeshAdapter();
var adapter = new LandblockSpawnAdapter(meshes);
var first = new LoadedLandblock(
0x1234FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>());
var second = new LoadedLandblock(
0x1235FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>());
adapter.OnLandblockLoaded(first, new[] { sharedGeometryId, sharedGeometryId });
adapter.OnLandblockLoaded(second, new[] { sharedGeometryId });
Assert.Equal(2, meshes.ReferenceCounts[sharedGeometryId]);
Assert.Equal(2, meshes.PreparedPinCalls.Count);
adapter.OnLandblockUnloaded(first.LandblockId);
Assert.Equal(1, meshes.ReferenceCounts[sharedGeometryId]);
adapter.OnLandblockUnloaded(second.LandblockId);
Assert.DoesNotContain(sharedGeometryId, meshes.ReferenceCounts);
}
private sealed class ReadinessMeshAdapter : IWbMeshAdapter
{
public HashSet<ulong> ReadyIds { get; } = new();
public Dictionary<ulong, int> ReferenceCounts { get; } = new();
public List<ulong> PreparedPinCalls { get; } = new();
public void IncrementRefCount(ulong id) =>
ReferenceCounts[id] = ReferenceCounts.GetValueOrDefault(id) + 1;
public void PinPreparedRenderData(ulong id)
{
PreparedPinCalls.Add(id);
IncrementRefCount(id);
}
public void DecrementRefCount(ulong id)
{
int count = ReferenceCounts[id] - 1;
if (count == 0) ReferenceCounts.Remove(id);
else ReferenceCounts[id] = count;
}
public bool IsRenderDataReady(ulong id) => ReadyIds.Contains(id);
}
}

View file

@ -0,0 +1,69 @@
using AcDream.App.Rendering.Wb;
using AcDream.Content;
using Chorizite.Core.Render.Enums;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class MeshUploadCachesTests
{
[Fact]
public void CpuCacheHit_AfterCompletedUpload_ReStagesExactlyOnceWithTexturePayload()
{
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 2);
byte[] textureBytes = [1, 2, 3, 4];
var data = new ObjectMeshData { ObjectId = 0x01000010ul, UploadAttempts = 2 };
data.TextureBatches[(1, 1, TextureFormat.RGBA8)] =
[
new TextureBatchData { TextureData = textureBytes },
];
cache.Store(data);
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out var initial));
Assert.Same(data, initial);
staging.Complete(data.ObjectId); // GPU upload completed, then was later evicted.
data.UploadAttempts = 3;
Assert.True(cache.TryGetAndStage(data.ObjectId, staging, out var cached));
Assert.Same(data, cached);
Assert.Equal(0, data.UploadAttempts);
Assert.Equal(textureBytes, cached!.TextureBatches.Values.Single().Single().TextureData);
Assert.True(cache.TryGetAndStage(data.ObjectId, staging, out _));
Assert.True(staging.TryDequeue(out var restaged));
Assert.Same(data, restaged);
Assert.False(staging.TryDequeue(out _));
}
[Fact]
public void Retry_RetainsQueueOwnershipUntilCompletion()
{
var staging = new MeshUploadStagingQueue();
var data = new ObjectMeshData { ObjectId = 0x01000010ul };
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out var attempt));
staging.Requeue(attempt!);
Assert.False(staging.Stage(data));
Assert.True(staging.TryDequeue(out var retry));
Assert.Same(data, retry);
staging.Complete(data.ObjectId);
Assert.True(staging.Stage(data));
}
[Fact]
public void UploadCompletion_NeverManufacturesLogicalOwnership()
{
const ulong id = 0x01000010ul;
var ownership = new MeshOwnershipCounter();
Assert.Equal(1, ownership.Acquire(id));
Assert.True(ownership.MarkUploadComplete(id));
Assert.Equal(1, ownership.Count(id));
Assert.Equal(0, ownership.Release(id));
Assert.False(ownership.MarkUploadComplete(id)); // late upload after unload
Assert.Equal(0, ownership.Count(id));
}
}