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

@ -15,13 +15,14 @@ namespace AcDream.App.Rendering.Wb;
/// <para>
/// On load: walks the landblock's atlas-tier entities, collects unique
/// GfxObj ids from their <c>MeshRefs</c>, calls
/// <c>IncrementRefCount</c> per id. Snapshots the id-set per landblock so
/// unload can match the load 1:1.
/// <c>IncrementRefCount</c> per id, and pins each specialized EnvCell geometry
/// id without starting generic GfxObj decode. Snapshots both id-sets per
/// landblock so unload can match the load 1:1.
/// </para>
///
/// <para>
/// On unload: looks up the snapshot, calls <c>DecrementRefCount</c> per id,
/// drops the snapshot. Unknown / never-loaded landblocks no-op.
/// On unload: looks up both snapshots, calls <c>DecrementRefCount</c> per id,
/// drops the snapshots. Unknown / never-loaded landblocks no-op.
/// </para>
///
/// <para>
@ -34,11 +35,9 @@ namespace AcDream.App.Rendering.Wb;
/// </para>
///
/// <para>
/// Thread safety: the underlying <see cref="IWbMeshAdapter"/> implementation
/// uses <c>ConcurrentDictionary</c>, so the streaming worker thread may call
/// this safely. The internal snapshot dictionary is NOT thread-safe and must
/// be called from a single streaming thread (the same thread that fires
/// AddLandblock / RemoveLandblock events).
/// Thread safety: the internal snapshots are intentionally not synchronized.
/// <see cref="AcDream.App.Streaming.GpuWorldState"/> invokes every load, unload, and readiness query
/// on the owning render/update thread.
/// </para>
/// </summary>
public sealed class LandblockSpawnAdapter
@ -48,6 +47,10 @@ public sealed class LandblockSpawnAdapter
// Maps landblock id → unique GfxObj ids registered for that landblock.
// Written on load, read+cleared on unload. Single-threaded (streaming worker).
private readonly Dictionary<uint, HashSet<ulong>> _idsByLandblock = new();
// EnvCell shells are prepared through PrepareEnvCellGeomMeshDataAsync rather
// than generic GfxObj loading, but still require explicit lifetime pins.
// Keep their synthetic ids separate so registration uses the no-decode pin.
private readonly Dictionary<uint, HashSet<ulong>> _additionalReadinessIdsByLandblock = new();
public LandblockSpawnAdapter(IWbMeshAdapter adapter)
{
@ -61,7 +64,9 @@ public sealed class LandblockSpawnAdapter
/// unique atlas-tier GfxObj id that has not already been registered for
/// this landblock.
/// </summary>
public void OnLandblockLoaded(LoadedLandblock landblock)
public void OnLandblockLoaded(
LoadedLandblock landblock,
IEnumerable<ulong>? additionalReadinessIds = null)
{
System.ArgumentNullException.ThrowIfNull(landblock);
@ -80,14 +85,51 @@ public sealed class LandblockSpawnAdapter
{
_idsByLandblock[landblock.LandblockId] = unique;
foreach (var id in unique) _adapter.IncrementRefCount(id);
return;
}
else
{
foreach (var id in unique)
{
if (registered.Add(id))
_adapter.IncrementRefCount(id);
}
}
foreach (var id in unique)
if (!_additionalReadinessIdsByLandblock.TryGetValue(
landblock.LandblockId,
out var additional))
{
if (registered.Add(id))
_adapter.IncrementRefCount(id);
additional = new HashSet<ulong>();
_additionalReadinessIdsByLandblock[landblock.LandblockId] = additional;
}
if (additionalReadinessIds is not null)
{
foreach (var id in additionalReadinessIds)
if (additional.Add(id))
_adapter.PinPreparedRenderData(id);
}
}
/// <summary>
/// True only after every render mesh required by this published landblock
/// is drawable. This includes both ref-counted static GfxObjs and EnvCell
/// shell geometry prepared by the independent indoor pipeline.
/// </summary>
public bool IsLandblockRenderReady(uint landblockId)
{
if (!_idsByLandblock.TryGetValue(landblockId, out var registered)
|| !_additionalReadinessIdsByLandblock.TryGetValue(landblockId, out var additional))
{
return false;
}
foreach (var id in registered)
if (!_adapter.IsRenderDataReady(id))
return false;
foreach (var id in additional)
if (!_adapter.IsRenderDataReady(id))
return false;
return true;
}
/// <summary>
@ -99,6 +141,9 @@ public sealed class LandblockSpawnAdapter
{
if (!_idsByLandblock.TryGetValue(landblockId, out var unique)) return;
foreach (var id in unique) _adapter.DecrementRefCount(id);
if (_additionalReadinessIdsByLandblock.TryGetValue(landblockId, out var additional))
foreach (var id in additional) _adapter.DecrementRefCount(id);
_idsByLandblock.Remove(landblockId);
_additionalReadinessIdsByLandblock.Remove(landblockId);
}
}