using System.Numerics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
using AcDream.Core.Lighting;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
///
/// Camera contract for retail CreatureMode-style UI viewports. The
/// renderer supplies the authored viewport aspect before each draw and uses
/// when it publishes the private scene lighting UBO.
///
internal interface IPrivateEntityViewportCamera : ICamera
{
Vector3 Eye { get; }
}
///
/// Shared render-to-texture implementation for the private 3-D creature
/// viewports used by paperdoll and examination UI. Each instance owns one
/// per encountered GPU flight slot, one
/// synthetic render identity, and one balanced texture-owner lease.
///
/// Campaign V slice V6k (V4g's first half). The target used to be a
/// hand-rolled FBO, colour texture and depth renderbuffer, and the resulting GL
/// texture name was published to the retained UI through
/// GlGpuDevice.RegisterExternalColorTexture — the pre-approved
/// transitional seam plan §7.1's final paragraph created, whose stated end was
/// exactly this slice. Both are gone: the device creates the target, the pass
/// DECLARES it, and the colour attachment is registered into the global texture
/// table like any other texture.
///
/// Declaring the target is what discharges §5.4. That section's divergence
/// — GL's BeginPass not binding framebuffer 0 for a null target, so this
/// renderer's own FBO survived — was reverted with V4c and is not on the tree;
/// what remained was the reason it had existed, namely that this renderer bound
/// a framebuffer the RHI knew nothing about. It now names its target, so
/// nothing depends on inheritance and the GL and Vulkan backends agree about
/// what Target: null means.
///
/// The GL arm this renderer used to open an RHI pass and delegate a
/// raw-GL WbDrawDispatcher into (through V10, §5.5.6) was deleted at
/// Campaign V slice V11: WbDrawDispatcher now records into the pass
/// this renderer publishes on both call sites the same way.
///
///
/// Campaign CC gate round 1, Batch D (GF-7/GF-14). Retail's
/// gmCG3DView::Update @0x004EE9D0 draws a SECOND private entity — a
/// heritage-authored environment Setup (m_pbgObject) — behind the main
/// one, in the SAME creature_mode_objects list. This renderer now
/// supports that as an OPTIONAL second entity slot, reserved at construction
/// via -shaped ctor param (see below) —
/// paperdoll and creature-appraisal never pass one, so
/// throws for them rather than silently doing nothing (the slot does not
/// exist). See for
/// the full decomp citation of the backdrop's placement (unposed, at the
/// scene origin, added to the draw list BEFORE the main entity).
///
///
internal sealed class PrivateEntityViewportRenderer :
IUiViewportRenderer,
IDisposable
{
private const uint PrivateLandblockId = 0u;
private readonly ICurrentGpuFrameSource _frames;
///
/// The world pass scope this renderer's own pass publishes into for the
/// span of the draw. WbDrawDispatcher borrows its pass from the
/// scope rather than opening one, so this renderer — which opens a pass of
/// its own to draw into an offscreen target — has to publish it there.
///
/// The raw-GL arm this used to be optional for (the GL dispatcher
/// recorded against whatever framebuffer was already bound, needing no
/// publication) was deleted at Campaign V slice V11.
///
private readonly IWorldPassScope _scope;
private readonly WbDrawDispatcher _dispatcher;
private readonly SceneLightingUboBinding _lightUbo;
private readonly IWbMeshAdapter _meshAdapter;
private readonly IPrivateEntityViewportCamera _camera;
private readonly HashSet _animatedIds;
private readonly string _diagnosticName;
private readonly EntitySlot _mainSlot;
/// Null for every renderer that never reserved a
/// backdropRenderId (paperdoll, creature-appraisal) — the backdrop
/// feature does not exist for them, not just "unused".
private readonly EntitySlot? _backdropSlot;
// A target written by frame N cannot also be sampled by an unretired frame
// N-1. Vulkan permits those command buffers to overlap, so one shared image
// is a cross-frame write/read race. The current frame slot selects one
// bounded target + texture-table handle; the retained UI samples that exact
// handle later in the same command buffer.
private readonly PrivateViewportFlightTargets _flightTargets;
public PrivateEntityViewportRenderer(
IWorldPassScope scope,
IGpuDevice device,
ICurrentGpuFrameSource frames,
WbDrawDispatcher dispatcher,
SceneLightingUboBinding lightUbo,
IEntityTextureLifetime textureLifetime,
IWbMeshAdapter meshAdapter,
uint renderId,
IPrivateEntityViewportCamera camera,
string diagnosticName,
uint? backdropRenderId = null)
{
if (renderId == 0u)
throw new ArgumentOutOfRangeException(nameof(renderId));
if (backdropRenderId == 0u)
throw new ArgumentOutOfRangeException(nameof(backdropRenderId));
_scope = scope ?? throw new ArgumentNullException(
nameof(scope),
"The viewport must publish a world pass scope to draw into.");
ArgumentNullException.ThrowIfNull(device);
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo));
_meshAdapter = meshAdapter
?? throw new ArgumentNullException(nameof(meshAdapter));
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
_diagnosticName = string.IsNullOrWhiteSpace(diagnosticName)
? "creature viewport"
: diagnosticName;
_flightTargets = new PrivateViewportFlightTargets(
device,
_diagnosticName);
IEntityTextureLifetime textureLifetimeChecked = textureLifetime
?? throw new ArgumentNullException(nameof(textureLifetime));
_mainSlot = new EntitySlot(_meshAdapter, textureLifetimeChecked, renderId, _diagnosticName);
_backdropSlot = backdropRenderId is uint backdropId
? new EntitySlot(_meshAdapter, textureLifetimeChecked, backdropId, _diagnosticName + " backdrop")
: null;
// F14 (Campaign CC gate round 1 closeout): this set is built ONCE
// here, from the RESERVED backdropRenderId (a renderer either has a
// backdrop slot or it doesn't — see _backdropSlot's own doc), not
// from whether a backdrop ENTITY is currently set via
// SetBackdrop/BuildDrawEntities. That is deliberately harmless, not
// an oversight: BuildDrawEntities below already degrades to
// [main] alone whenever the backdrop slot is null or has no
// meshes, so animatedEntityIds carrying a backdrop id with no
// matching entry in THIS frame's actual draw-entities list is a
// pure dead lookup (WbDrawDispatcher.Draw only ever consults this
// set against ids it is ACTUALLY drawing) — never a wrong-entity
// animation flag, never extra per-frame work beyond one inert
// HashSet entry. Recomputing per-frame would add real complexity
// (a second HashSet allocation or a mutable-set sync path) for a
// case that is already correct by construction.
_animatedIds = backdropRenderId is uint animatedBackdropId
? [renderId, animatedBackdropId]
: [renderId];
}
///
/// A GL framebuffer's colour texture used to sample bottom-up; that arm was
/// deleted at Campaign V slice V11, and a Vulkan render target's does not.
/// See .
///
public bool TextureIsBottomUp => false;
public void SetEntity(WorldEntity? entity)
{
_mainSlot.Set(entity);
if (entity is null)
{
// A character-session reset explicitly invalidates the sampled
// scenes. Do not let a replacement that is still uploading expose
// the previous character through any flight target.
_flightTargets.InvalidateCompletedScenes();
}
}
///
/// Advances the private entity's mesh and texture-composite readiness
/// without allocating or clearing a render target. Paperdoll uses this
/// while its tab is hidden so first-open work is already resident.
///
public bool Prepare()
{
if (!_mainSlot.PrepareForDraw()
|| !(_backdropSlot?.PrepareForDraw() ?? true))
{
return false;
}
WorldEntity? entity = _mainSlot.Entity;
if (entity is null || entity.MeshRefs.Count == 0)
{
return false;
}
IReadOnlyList entities = BuildDrawEntities(
_backdropSlot?.Entity,
entity);
return _dispatcher.PreparePrivateEntityResources(entities);
}
///
/// Sets or clears the environment backdrop entity drawn BEHIND the main
/// entity — GF-7/GF-14's fix, retail's gmCG3DView::m_pbgObject. Only
/// valid on a renderer constructed with a backdropRenderId
/// ('s own construction); calling this
/// on a renderer that never reserved one (paperdoll, creature-appraisal)
/// throws — the slot does not exist for them, so there is nothing to make
/// "inert" by silently ignoring the call instead.
///
public void SetBackdrop(WorldEntity? entity)
{
if (_backdropSlot is null)
{
throw new InvalidOperationException(
$"The {_diagnosticName} was not constructed with a "
+ "backdropRenderId and cannot render a second (backdrop) entity.");
}
_backdropSlot.Set(entity);
}
///
/// Renders the entity and returns the the
/// retained UI blits — a one-based index into the device's global texture
/// table, not a GL texture name. Zero means nothing was rendered.
///
public uint Render(int width, int height)
{
if (width <= 0 || height <= 0)
return 0u;
IGpuFrame frame = _frames.CurrentFrame
?? throw new InvalidOperationException(
$"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
int frameSlot = frame.SlotIndex;
// #443: acquiring a synthetic mesh reference only schedules CPU
// preparation/GPU upload; it does not make the mesh drawable. Keep the
// current flight slot's 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 cleared target.
bool mainReady = _mainSlot.PrepareForDraw();
bool backdropReady = _backdropSlot?.PrepareForDraw() ?? true;
if (!mainReady || !backdropReady)
{
return _mainSlot.Entity is not null
? _flightTargets.CompletedHandle(frameSlot)
: 0u;
}
WorldEntity? entity = _mainSlot.Entity;
if (entity is null || entity.MeshRefs.Count == 0)
return 0u;
IReadOnlyList drawEntities = BuildDrawEntities(
_backdropSlot?.Entity,
entity);
if (!_dispatcher.PreparePrivateEntityResources(drawEntities))
return _flightTargets.CompletedHandle(frameSlot);
PrivateViewportFlightTargets.TargetSlot? targetSlot =
_flightTargets.Ensure(frameSlot, width, height);
if (targetSlot is null)
return 0u;
_camera.Aspect = width / (float)height;
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
{
Name = _diagnosticName,
Color = new GpuColorAttachment(
Target: targetSlot.Target,
Load: GpuLoadOp.Clear,
Store: GpuStoreOp.Store,
ClearColor: Vector4.Zero),
Depth = new GpuDepthAttachment(
Load: GpuLoadOp.Clear,
Store: GpuStoreOp.DontCare,
ClearDepth: 1f,
ClearStencil: 0),
SampleCount = 1,
});
// The dispatcher borrows its pass from the scope, so this pass has to
// BE the scope's for the span of the draw. Published after the world
// phase has closed its own, which is why it does not nest; the
// sections it resets are republished by UploadCreatureLight
// immediately below, which is the private lighting this view wants
// rather than the world's.
using IDisposable publication = _scope.Publish(encoder);
UploadCreatureLight();
var entries =
new (uint, Vector3, Vector3, IReadOnlyList,
IReadOnlyDictionary?)[]
{
(
PrivateLandblockId,
new Vector3(-1024f),
new Vector3(1024f),
drawEntities,
null),
};
// #443: a private pass must not append its transforms into the shared
// world transform frame — the default mesh shaders index parallel
// per-instance arrays zero-based, so a non-zero arena base zeroes the
// doll's per-instance opacity and the target stays blank whenever a
// world frame is active. See NextClassicDrawIsPrivatePass.
_dispatcher.NextClassicDrawIsPrivatePass = true;
_dispatcher.Draw(
_camera,
entries,
frustum: null,
neverCullLandblockId: PrivateLandblockId,
visibleCellIds: null,
animatedEntityIds: _animatedIds);
targetSlot.HasRenderedScene = true;
return UiTextureTableHandle.FromSlot(targetSlot.TextureSlot);
}
///
/// Pure helper assembling this frame's draw-entity list in retail's own
/// insertion order — gmCG3DView::Update adds the backdrop object to
/// creature_mode_objects BEFORE the main (player) object is
/// re-added (the player's own re-AddObject happens much later, at
/// ~0x004ef199, after the full clothing ObjDesc composes — see
/// 's own decomp
/// citation). A null or empty-meshed backdrop degrades to exactly the main
/// entity — this is the paperdoll/creature-appraisal invariant (they never
/// configure a backdrop slot at all, so this always takes this branch for
/// them), pinned directly by
/// PrivateEntityViewportRendererDrawOrderTests without needing a
/// live GPU device or a constructed .
///
internal static IReadOnlyList BuildDrawEntities(WorldEntity? backdrop, WorldEntity main) =>
backdrop is not null && backdrop.MeshRefs.Count > 0
? [backdrop, main]
: [main];
///
/// Both retail paperdoll and creature examination call
/// UIElement_Viewport::SetLight(DISTANT_LIGHT, 2, (0.3,1.9,0.65)).
/// Byte-decoded confirmation (Batch D re-derivation): the SAME three
/// float32 constants (0x3e99999a/0x3ff33333/0x3F266666
/// = 0.3/1.9/0.65) appear verbatim at gmCG3DView::Update's own
/// SetLight call site (pseudo-C ~0x004eecd3-0x004eece3) — the
/// chargen preview uses the EXACT same light this method already ported,
/// not a different value.
///
private void UploadCreatureLight()
{
Vector3 direction = Vector3.Normalize(new Vector3(0.3f, 1.9f, 0.65f));
_lightUbo.Upload(new SceneLightingUbo
{
Light0 = new UboLight
{
PosAndKind = Vector4.Zero,
DirAndRange = new Vector4(direction, 1e9f),
ColorAndIntensity = new Vector4(1f, 1f, 1f, 2f),
ConeAngleEtc = Vector4.Zero,
},
CellAmbient = new Vector4(0.3f, 0.3f, 0.3f, 1f),
FogParams = new Vector4(1e9f, 1e9f, 0f, 0f),
FogColor = Vector4.Zero,
CameraAndTime = new Vector4(_camera.Eye, 0f),
});
}
public void Dispose()
{
List? failures = null;
try
{
_mainSlot.Dispose();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
try
{
_backdropSlot?.Dispose();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
try
{
_flightTargets.Dispose();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
if (failures is not null)
{
throw new AggregateException(
$"The {_diagnosticName} resources did not fully release.",
failures);
}
}
///
/// Bounded render-target ownership keyed by .
/// A frame slot is reopened only after its previous submission retires, so
/// the target selected here can be written and sampled within that frame
/// without racing a different in-flight command buffer.
///
internal sealed class PrivateViewportFlightTargets : IDisposable
{
internal sealed class TargetSlot(
IGpuRenderTarget target,
GpuTextureSlot textureSlot)
{
internal IGpuRenderTarget Target { get; } = target;
internal GpuTextureSlot TextureSlot { get; } = textureSlot;
internal bool HasRenderedScene { get; set; }
}
private readonly IGpuDevice _device;
private readonly string _diagnosticName;
private readonly List _slots = [];
private int _width;
private int _height;
private bool _disposed;
internal PrivateViewportFlightTargets(
IGpuDevice device,
string diagnosticName)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
_diagnosticName = string.IsNullOrWhiteSpace(diagnosticName)
? "creature viewport"
: diagnosticName;
}
internal int AllocatedSlotCount =>
_slots.Count(static slot => slot is not null);
internal TargetSlot? Ensure(int frameSlot, int width, int height)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height);
if (_width != 0 && (_width != width || _height != height))
ReleaseAll();
while (_slots.Count <= frameSlot)
_slots.Add(null);
if (_slots[frameSlot] is { } existing)
return existing;
IGpuRenderTarget target;
try
{
target = _device.CreateRenderTarget(
new GpuRenderTargetDescription(
$"{_diagnosticName}-flight-{frameSlot}",
width,
height,
GpuTextureFormat.Rgba8UnormRenderTarget,
// Depth24Stencil8, as the original private viewport
// renderbuffer was. Nothing samples this attachment.
GpuTextureFormat.Depth24Stencil8,
SampleCount: 1));
}
catch (Exception failure)
{
Console.WriteLine(
$"[{_diagnosticName}] render target unavailable "
+ $"({width}x{height}, flight {frameSlot}): {failure.Message}");
return null;
}
try
{
// The device de-duplicates immutable samplers. Retained UI
// blits this target through its ordinary texture-table entry.
IGpuSampler sampler = _device.CreateSampler(
GpuSamplerDescription.WorldClamp);
GpuTextureSlot textureSlot = _device.RegisterTexture(
target.ColorTexture,
sampler);
var created = new TargetSlot(target, textureSlot);
_slots[frameSlot] = created;
_width = width;
_height = height;
return created;
}
catch
{
target.Dispose();
throw;
}
}
internal uint CompletedHandle(int frameSlot)
{
if ((uint)frameSlot >= (uint)_slots.Count
|| _slots[frameSlot] is not { HasRenderedScene: true } slot)
{
return 0u;
}
return UiTextureTableHandle.FromSlot(slot.TextureSlot);
}
internal void InvalidateCompletedScenes()
{
for (int i = 0; i < _slots.Count; i++)
{
if (_slots[i] is { } slot)
slot.HasRenderedScene = false;
}
}
private void ReleaseAll()
{
List? failures = null;
for (int i = 0; i < _slots.Count; i++)
{
TargetSlot? slot = _slots[i];
if (slot is null)
continue;
try
{
_device.ReleaseTextureSlot(slot.TextureSlot);
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
try
{
slot.Target.Dispose();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
_slots.Clear();
_width = 0;
_height = 0;
if (failures is { Count: > 0 })
throw new AggregateException(failures);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
ReleaseAll();
}
}
///
/// 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
/// are resident. Kept internal so #443's lifetime/readiness behavior can
/// be pinned without constructing a live GPU device.
///
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 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);
}
///
/// 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.
///
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 _retiringMeshReferences = [];
private SyntheticEntityMeshReferenceOwner? _meshReferences;
private MeshSnapshot? _meshSnapshot;
private PendingEntity? _pending;
public EntitySlot(
IWbMeshAdapter meshAdapter,
IEntityTextureLifetime textureLifetime,
uint ownerLocalId,
string diagnosticName)
{
_meshAdapter = meshAdapter;
_textureOwnerLease = new FixedEntityTextureOwnerLease(textureLifetime, ownerLocalId);
_diagnosticName = diagnosticName;
}
public WorldEntity? Entity { get; private set; }
internal bool HasPending => _pending is not null;
public void Set(WorldEntity? entity)
{
ReleaseRetiringMeshReferences();
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;
if (ReferenceEquals(Entity, entity)
&& _meshSnapshot?.Matches(entity) == true)
{
ReleasePending();
return;
}
if (_pending is { } pending
&& ReferenceEquals(pending.Entity, entity)
&& pending.Snapshot.Matches(entity))
{
return;
}
Stage(entity);
}
///
/// 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.
///
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
{
replacement.Acquire();
}
catch (Exception acquisitionFailure)
{
try
{
replacement.Dispose();
}
catch (Exception rollbackFailure)
{
_retiringMeshReferences.Add(replacement);
throw new AggregateException(
$"The {_diagnosticName} candidate mesh acquisition failed "
+ "and its rollback did not converge.",
acquisitionFailure,
rollbackFailure);
}
System.Runtime.ExceptionServices.ExceptionDispatchInfo
.Capture(acquisitionFailure)
.Throw();
throw new InvalidOperationException("Unreachable exception dispatch path.");
}
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
{
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? failures = null;
try
{
_textureOwnerLease.Dispose();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
try
{
ReleaseRetiringMeshReferences();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
if (failures is not null)
{
throw new AggregateException(
$"The {_diagnosticName} resources did not fully release.",
failures);
}
}
private void ReleaseRetiringMeshReferences()
{
List? failures = null;
for (int i = _retiringMeshReferences.Count - 1; i >= 0; i--)
{
SyntheticEntityMeshReferenceOwner owner =
_retiringMeshReferences[i];
try
{
owner.Dispose();
if (owner.IsDisposed)
_retiringMeshReferences.RemoveAt(i);
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
if (failures is not null)
{
throw new AggregateException(
$"One or more {_diagnosticName} mesh owners remain pending.",
failures);
}
}
}
}