fix(ui): wait for private viewport mesh residency
This commit is contained in:
parent
82e4b4cb6d
commit
f160f3fee1
3 changed files with 476 additions and 63 deletions
|
|
@ -99,6 +99,7 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
private GpuTextureSlot _slot = GpuTextureSlot.Unassigned;
|
||||
private int _fbW;
|
||||
private int _fbH;
|
||||
private bool _hasRenderedScene;
|
||||
|
||||
public PrivateEntityViewportRenderer(
|
||||
IWorldPassScope scope,
|
||||
|
|
@ -197,6 +198,23 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
/// </summary>
|
||||
public uint Render(int width, int height)
|
||||
{
|
||||
// #443: acquiring a synthetic mesh reference only schedules CPU
|
||||
// preparation/GPU upload; it does not make the mesh drawable. Keep the
|
||||
// last completed private scene intact until every drawable mesh in the
|
||||
// replacement has crossed that upload barrier. On first open there is
|
||||
// no completed scene, so return zero and let the authored panel art
|
||||
// show through instead of publishing a freshly-cleared black target.
|
||||
bool mainReady = _mainSlot.PrepareForDraw();
|
||||
bool backdropReady = _backdropSlot?.PrepareForDraw() ?? true;
|
||||
if (!mainReady || !backdropReady)
|
||||
{
|
||||
return _mainSlot.Entity is not null
|
||||
&& _hasRenderedScene
|
||||
&& _slot.IsAssigned
|
||||
? UiTextureTableHandle.FromSlot(_slot)
|
||||
: 0u;
|
||||
}
|
||||
|
||||
WorldEntity? entity = _mainSlot.Entity;
|
||||
if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0)
|
||||
return 0u;
|
||||
|
|
@ -256,6 +274,7 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
neverCullLandblockId: PrivateLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: _animatedIds);
|
||||
_hasRenderedScene = true;
|
||||
return UiTextureTableHandle.FromSlot(_slot);
|
||||
}
|
||||
|
||||
|
|
@ -367,6 +386,7 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
_target = null;
|
||||
_fbW = 0;
|
||||
_fbH = 0;
|
||||
_hasRenderedScene = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -405,31 +425,88 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ulong> CollectMeshIds(WorldEntity entity)
|
||||
{
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
yield return entity.MeshRefs[i].GfxObjId;
|
||||
for (int i = 0; i < entity.PartOverrides.Count; i++)
|
||||
yield return entity.PartOverrides[i].GfxObjId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One private entity's own mesh-reference/texture-owner lifetime,
|
||||
/// independent of any other slot on the same renderer. Factored out at
|
||||
/// Campaign CC gate round 1 Batch D so the chargen backdrop entity gets
|
||||
/// the EXACT SAME acquire/replace/retire behavior the main entity already
|
||||
/// had — a single-owner class shared by both slots rather than a second,
|
||||
/// hand-duplicated copy of <see cref="PrivateEntityViewportRenderer.SetEntity"/>'s
|
||||
/// pre-Batch-D body.
|
||||
/// One private entity's mesh-reference/texture-owner lifetime, independent
|
||||
/// of every other slot on the renderer. Publication is two-phase: a candidate owns its mesh
|
||||
/// references while preparation/upload runs, but does not replace the
|
||||
/// drawable entity until all of its actual <see cref="WorldEntity.MeshRefs"/>
|
||||
/// are resident. Kept internal so #443's lifetime/readiness behavior can
|
||||
/// be pinned without constructing a live GPU device.
|
||||
/// </summary>
|
||||
private sealed class EntitySlot
|
||||
internal sealed class EntitySlot
|
||||
{
|
||||
private sealed class MeshSnapshot
|
||||
{
|
||||
private readonly ulong[] _ownedIds;
|
||||
private readonly int _drawMeshCount;
|
||||
|
||||
private MeshSnapshot(ulong[] ownedIds, int drawMeshCount)
|
||||
{
|
||||
_ownedIds = ownedIds;
|
||||
_drawMeshCount = drawMeshCount;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ulong> OwnedIds => _ownedIds;
|
||||
|
||||
public static MeshSnapshot Capture(WorldEntity entity)
|
||||
{
|
||||
int drawMeshCount = entity.MeshRefs.Count;
|
||||
var ids = new ulong[drawMeshCount + entity.PartOverrides.Count];
|
||||
for (int i = 0; i < drawMeshCount; i++)
|
||||
ids[i] = entity.MeshRefs[i].GfxObjId;
|
||||
for (int i = 0; i < entity.PartOverrides.Count; i++)
|
||||
ids[drawMeshCount + i] = entity.PartOverrides[i].GfxObjId;
|
||||
return new MeshSnapshot(ids, drawMeshCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Residency identity deliberately excludes part transforms,
|
||||
/// palette ranges and surface overrides: changing those does not
|
||||
/// require another mesh upload. A different entity instance still
|
||||
/// stages a replacement so its fixed texture owner is refreshed.
|
||||
/// </summary>
|
||||
public bool Matches(WorldEntity entity)
|
||||
{
|
||||
if (entity.MeshRefs.Count != _drawMeshCount
|
||||
|| entity.PartOverrides.Count != _ownedIds.Length - _drawMeshCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _drawMeshCount; i++)
|
||||
if (entity.MeshRefs[i].GfxObjId != _ownedIds[i])
|
||||
return false;
|
||||
for (int i = 0; i < entity.PartOverrides.Count; i++)
|
||||
if (entity.PartOverrides[i].GfxObjId != _ownedIds[_drawMeshCount + i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AreDrawMeshesReady(IWbMeshAdapter adapter)
|
||||
{
|
||||
for (int i = 0; i < _drawMeshCount; i++)
|
||||
{
|
||||
ulong id = _ownedIds[i];
|
||||
if (id == 0u || !adapter.IsRenderDataReady(id))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PendingEntity(
|
||||
WorldEntity Entity,
|
||||
MeshSnapshot Snapshot,
|
||||
SyntheticEntityMeshReferenceOwner MeshReferences);
|
||||
|
||||
private readonly IWbMeshAdapter _meshAdapter;
|
||||
private readonly FixedEntityTextureOwnerLease _textureOwnerLease;
|
||||
private readonly string _diagnosticName;
|
||||
private readonly List<SyntheticEntityMeshReferenceOwner> _retiringMeshReferences = [];
|
||||
|
||||
private SyntheticEntityMeshReferenceOwner? _meshReferences;
|
||||
private MeshSnapshot? _meshSnapshot;
|
||||
private PendingEntity? _pending;
|
||||
|
||||
public EntitySlot(
|
||||
IWbMeshAdapter meshAdapter,
|
||||
|
|
@ -444,75 +521,181 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
|
||||
public WorldEntity? Entity { get; private set; }
|
||||
|
||||
internal bool HasPending => _pending is not null;
|
||||
|
||||
public void Set(WorldEntity? entity)
|
||||
{
|
||||
ReleaseRetiringMeshReferences();
|
||||
|
||||
if (ReferenceEquals(Entity, entity))
|
||||
if (entity is null)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// The common animated appraisal path reuses one clone object. Let
|
||||
// PrepareForDraw perform its allocation-free mesh-id comparison;
|
||||
// if no candidate is pending, reference equality is enough here.
|
||||
if (ReferenceEquals(Entity, entity) && _pending is null)
|
||||
return;
|
||||
|
||||
SyntheticEntityMeshReferenceOwner? replacement = null;
|
||||
if (entity is not null)
|
||||
if (ReferenceEquals(Entity, entity)
|
||||
&& _meshSnapshot?.Matches(entity) == true)
|
||||
{
|
||||
replacement = new SyntheticEntityMeshReferenceOwner(
|
||||
_meshAdapter,
|
||||
CollectMeshIds(entity));
|
||||
replacement.Acquire();
|
||||
ReleasePending();
|
||||
return;
|
||||
}
|
||||
|
||||
SyntheticEntityMeshReferenceOwner? previous = _meshReferences;
|
||||
if (_pending is { } pending
|
||||
&& ReferenceEquals(pending.Entity, entity)
|
||||
&& pending.Snapshot.Matches(entity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Stage(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes an in-place MeshRefs mutation, re-arms missing uploads,
|
||||
/// and atomically promotes a fully drawable candidate. False tells the
|
||||
/// renderer to preserve its last completed render target this frame.
|
||||
/// </summary>
|
||||
public bool PrepareForDraw()
|
||||
{
|
||||
ReleaseRetiringMeshReferences();
|
||||
|
||||
if (_pending is { } pending)
|
||||
{
|
||||
if (!pending.Snapshot.Matches(pending.Entity))
|
||||
Stage(pending.Entity);
|
||||
}
|
||||
else if (Entity is { } current
|
||||
&& _meshSnapshot?.Matches(current) != true)
|
||||
{
|
||||
// Chargen animation and appraisal synchronization both mutate
|
||||
// a retained WorldEntity in place. Detect a changed GfxObj set
|
||||
// here even when the caller did not issue another Set call.
|
||||
Stage(current);
|
||||
}
|
||||
|
||||
pending = _pending;
|
||||
if (pending is null)
|
||||
return true;
|
||||
if (!pending.Snapshot.AreDrawMeshesReady(_meshAdapter))
|
||||
return false;
|
||||
|
||||
PromotePending(pending);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Stage(WorldEntity entity)
|
||||
{
|
||||
MeshSnapshot snapshot = MeshSnapshot.Capture(entity);
|
||||
var replacement = new SyntheticEntityMeshReferenceOwner(
|
||||
_meshAdapter,
|
||||
snapshot.OwnedIds);
|
||||
try
|
||||
{
|
||||
_textureOwnerLease.Replace(entity is not null);
|
||||
replacement.Acquire();
|
||||
}
|
||||
catch (Exception textureFailure)
|
||||
catch (Exception acquisitionFailure)
|
||||
{
|
||||
if (replacement is null)
|
||||
throw;
|
||||
|
||||
try
|
||||
{
|
||||
replacement.Dispose();
|
||||
}
|
||||
catch (Exception rollbackFailure)
|
||||
{
|
||||
_retiringMeshReferences.Add(replacement);
|
||||
throw new AggregateException(
|
||||
$"The {_diagnosticName} texture-owner replacement failed "
|
||||
+ "and the replacement mesh-owner rollback did not converge.",
|
||||
textureFailure,
|
||||
$"The {_diagnosticName} candidate mesh acquisition failed "
|
||||
+ "and its rollback did not converge.",
|
||||
acquisitionFailure,
|
||||
rollbackFailure);
|
||||
}
|
||||
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
||||
.Capture(textureFailure)
|
||||
.Capture(acquisitionFailure)
|
||||
.Throw();
|
||||
throw new InvalidOperationException("Unreachable exception dispatch path.");
|
||||
}
|
||||
|
||||
_meshReferences = replacement;
|
||||
Entity = entity;
|
||||
PendingEntity? previous = _pending;
|
||||
_pending = new PendingEntity(entity, snapshot, replacement);
|
||||
if (previous is not null)
|
||||
Retire(previous.MeshReferences);
|
||||
}
|
||||
|
||||
private void PromotePending(PendingEntity pending)
|
||||
{
|
||||
// Release the fixed texture owner's prior composites only at the
|
||||
// same atomic edge that publishes the new mesh set. If release
|
||||
// fails, the candidate remains pending and can retry intact.
|
||||
_textureOwnerLease.Replace(hasReplacement: true);
|
||||
|
||||
SyntheticEntityMeshReferenceOwner? previous = _meshReferences;
|
||||
_meshReferences = pending.MeshReferences;
|
||||
_meshSnapshot = pending.Snapshot;
|
||||
Entity = pending.Entity;
|
||||
_pending = null;
|
||||
|
||||
if (previous is not null)
|
||||
Retire(previous);
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
if (Entity is null && _pending is null)
|
||||
return;
|
||||
|
||||
ReleasePending();
|
||||
_textureOwnerLease.Replace(hasReplacement: false);
|
||||
|
||||
SyntheticEntityMeshReferenceOwner? previous = _meshReferences;
|
||||
_meshReferences = null;
|
||||
_meshSnapshot = null;
|
||||
Entity = null;
|
||||
if (previous is not null)
|
||||
Retire(previous);
|
||||
}
|
||||
|
||||
private void ReleasePending()
|
||||
{
|
||||
PendingEntity? pending = _pending;
|
||||
if (pending is null)
|
||||
return;
|
||||
_pending = null;
|
||||
Retire(pending.MeshReferences);
|
||||
}
|
||||
|
||||
private void Retire(SyntheticEntityMeshReferenceOwner owner)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
previous.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_retiringMeshReferences.Add(previous);
|
||||
throw;
|
||||
}
|
||||
owner.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_retiringMeshReferences.Add(owner);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Entity = null;
|
||||
_meshSnapshot = null;
|
||||
if (_meshReferences is { } current)
|
||||
{
|
||||
_meshReferences = null;
|
||||
_retiringMeshReferences.Add(current);
|
||||
}
|
||||
if (_pending is { } pending)
|
||||
{
|
||||
_pending = null;
|
||||
_retiringMeshReferences.Add(pending.MeshReferences);
|
||||
}
|
||||
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue