The remaining code-bearing findings from the round review, F4-F16 minus the doc-only items (batched separately): - F4: three client-wide UiButton corpus sweeps (LabelBox path — exactly the 4 Town buttons, confined to chargen; conflicting custom-selection- pair + standard Normal/Highlight media — zero found, no gate tightening needed; per-state label-color map — 209 matches beyond chargen, confirming AP-222's mechanism has always been broadly active since it shipped generically in DatWidgetFactory). - F5/F6: LayoutImporter's Batch C un-consumed-children carve-out now honors a child's own AuthoredInvisible flag (a narrow honor scoped to exactly that carve-out, not the general #408 client-wide one) — the chat transcript's new-text indicator (0x1000048C) was building as a visible phantom element retail never shows; verified both directions against the gold-frame pieces, which do not author Invisible. - F7: BoundedProcessOutputCapture.AppendLine combines the line text and its trailing newline into one buffer and one file open/write/close instead of two. - F9: corrected a stale comment in RuntimeSettingsTargets — #407 split DisplayModeCatalog's Resolutions/WindowedResolutions in two, so the fullscreen validator's own narrower list is now DELIBERATELY different from the Config dropdown's fuller offering, not the "must match" bug the comment described. - F10: documented (not changed) why the LabelBox path's default 3px inset and the face-relative +4px gap in DatWidgetFactory.BuildButton are deliberately different numbers — neither carries a retail citation, and moving either to match the other would be an unfounded guess on a button that currently works correctly. - F11: Heritage/Profession/Summary/Town description pages now compose DatRichText.Compose's result ONCE inside their already revision-gated Refresh, caching the built line list instead of re-wrapping on every draw call. - F14: documented (not changed) why PrivateEntityViewportRenderer's _animatedIds set carrying a reserved-but-never-drawn backdrop id is harmless — BuildDrawEntities already excludes a null/empty backdrop from the actual draw list, so the id is never looked up. - F16: the Summary preview now uses its own render-id pair (SummaryPreviewRenderId/SummaryPreviewBackdropRenderId, 0xDA11D035/ 0xDA11D036) instead of sharing the Appearance page's (0xDA11D032/0xDA11D034) — confirmed by tracing FixedEntityTextureOwnerLease through TextureCache to CompositeTextureArrayCache's shared owner tracker that both pages' previews share ONE process-wide TextureCache, so sharing render ids was a real cross-page texture-release collision (either page's own re-dress or disposal could release the OTHER page's still-active textures), not a theoretical one. F3's own register bookkeeping (AP-229 addendum) and F12's register/AD header-count corrections land in the docs-only commit alongside F15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
570 lines
22 KiB
C#
570 lines
22 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Camera contract for retail <c>CreatureMode</c>-style UI viewports. The
|
|
/// renderer supplies the authored viewport aspect before each draw and uses
|
|
/// <see cref="Eye"/> when it publishes the private scene lighting UBO.
|
|
/// </summary>
|
|
internal interface IPrivateEntityViewportCamera : ICamera
|
|
{
|
|
Vector3 Eye { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shared render-to-texture implementation for the private 3-D creature
|
|
/// viewports used by paperdoll and examination UI. Each instance owns one
|
|
/// <see cref="IGpuRenderTarget"/>, one synthetic render identity, and one
|
|
/// balanced texture-owner lease.
|
|
///
|
|
/// <para><b>Campaign V slice V6k (V4g's first half).</b> 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
|
|
/// <c>GlGpuDevice.RegisterExternalColorTexture</c> — 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.</para>
|
|
///
|
|
/// <para>Declaring the target is what discharges §5.4. That section's divergence
|
|
/// — GL's <c>BeginPass</c> 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 <c>Target: null</c> means.</para>
|
|
///
|
|
/// <para>The GL arm this renderer used to open an RHI pass and delegate a
|
|
/// raw-GL <c>WbDrawDispatcher</c> into (through V10, §5.5.6) was deleted at
|
|
/// Campaign V slice V11: <c>WbDrawDispatcher</c> now records into the pass
|
|
/// this renderer publishes on both call sites the same way.</para>
|
|
///
|
|
/// <para>
|
|
/// <b>Campaign CC gate round 1, Batch D (GF-7/GF-14).</b> Retail's
|
|
/// <c>gmCG3DView::Update @0x004EE9D0</c> draws a SECOND private entity — a
|
|
/// heritage-authored environment Setup (<c>m_pbgObject</c>) — behind the main
|
|
/// one, in the SAME <c>creature_mode_objects</c> list. This renderer now
|
|
/// supports that as an OPTIONAL second entity slot, reserved at construction
|
|
/// via <paramref name="backdropRenderId"/>-shaped ctor param (see below) —
|
|
/// paperdoll and creature-appraisal never pass one, so <see cref="SetBackdrop"/>
|
|
/// throws for them rather than silently doing nothing (the slot does not
|
|
/// exist). See <see cref="ChargenPreviewEntityBuilder.TryBuildBackdrop"/> for
|
|
/// the full decomp citation of the backdrop's placement (unposed, at the
|
|
/// scene origin, added to the draw list BEFORE the main entity).
|
|
/// </para>
|
|
/// </summary>
|
|
internal sealed class PrivateEntityViewportRenderer :
|
|
IUiViewportRenderer,
|
|
IDisposable
|
|
{
|
|
private const uint PrivateLandblockId = 0u;
|
|
|
|
private readonly IGpuDevice _device;
|
|
private readonly ICurrentGpuFrameSource _frames;
|
|
|
|
/// <summary>
|
|
/// The world pass scope this renderer's own pass publishes into for the
|
|
/// span of the draw. <c>WbDrawDispatcher</c> 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.
|
|
///
|
|
/// <para>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.</para>
|
|
/// </summary>
|
|
private readonly IWorldPassScope _scope;
|
|
|
|
private readonly WbDrawDispatcher _dispatcher;
|
|
private readonly SceneLightingUboBinding _lightUbo;
|
|
private readonly IWbMeshAdapter _meshAdapter;
|
|
private readonly IPrivateEntityViewportCamera _camera;
|
|
private readonly HashSet<uint> _animatedIds;
|
|
private readonly string _diagnosticName;
|
|
|
|
private readonly EntitySlot _mainSlot;
|
|
|
|
/// <summary>Null for every renderer that never reserved a
|
|
/// <c>backdropRenderId</c> (paperdoll, creature-appraisal) — the backdrop
|
|
/// feature does not exist for them, not just "unused".</summary>
|
|
private readonly EntitySlot? _backdropSlot;
|
|
|
|
private IGpuRenderTarget? _target;
|
|
private IGpuSampler? _sampler;
|
|
private GpuTextureSlot _slot = GpuTextureSlot.Unassigned;
|
|
private int _fbW;
|
|
private int _fbH;
|
|
|
|
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.");
|
|
_device = device ?? throw new ArgumentNullException(nameof(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;
|
|
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="IUiViewportRenderer.TextureIsBottomUp"/>.
|
|
/// </summary>
|
|
public bool TextureIsBottomUp => false;
|
|
|
|
public void SetEntity(WorldEntity? entity) => _mainSlot.Set(entity);
|
|
|
|
/// <summary>
|
|
/// Sets or clears the environment backdrop entity drawn BEHIND the main
|
|
/// entity — GF-7/GF-14's fix, retail's <c>gmCG3DView::m_pbgObject</c>. Only
|
|
/// valid on a renderer constructed with a <c>backdropRenderId</c>
|
|
/// (<see cref="ChargenPreviewRenderer"/>'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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the entity and returns the <see cref="UiTextureTableHandle"/> 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.
|
|
/// </summary>
|
|
public uint Render(int width, int height)
|
|
{
|
|
WorldEntity? entity = _mainSlot.Entity;
|
|
if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0)
|
|
return 0u;
|
|
|
|
EnsureRenderTarget(width, height);
|
|
if (_target is null)
|
|
return 0u;
|
|
_camera.Aspect = width / (float)height;
|
|
|
|
IGpuFrame frame = _frames.CurrentFrame
|
|
?? throw new InvalidOperationException(
|
|
$"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
|
|
|
|
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
|
{
|
|
Name = _diagnosticName,
|
|
Color = new GpuColorAttachment(
|
|
Target: _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();
|
|
|
|
IReadOnlyList<WorldEntity> drawEntities = BuildDrawEntities(_backdropSlot?.Entity, entity);
|
|
var entries =
|
|
new (uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
|
IReadOnlyDictionary<uint, WorldEntity>?)[]
|
|
{
|
|
(
|
|
PrivateLandblockId,
|
|
new Vector3(-1024f),
|
|
new Vector3(1024f),
|
|
drawEntities,
|
|
null),
|
|
};
|
|
|
|
_dispatcher.Draw(
|
|
_camera,
|
|
entries,
|
|
frustum: null,
|
|
neverCullLandblockId: PrivateLandblockId,
|
|
visibleCellIds: null,
|
|
animatedEntityIds: _animatedIds);
|
|
return UiTextureTableHandle.FromSlot(_slot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pure helper assembling this frame's draw-entity list in retail's own
|
|
/// insertion order — <c>gmCG3DView::Update</c> adds the backdrop object to
|
|
/// <c>creature_mode_objects</c> BEFORE the main (player) object is
|
|
/// re-added (the player's own re-<c>AddObject</c> happens much later, at
|
|
/// ~0x004ef199, after the full clothing ObjDesc composes — see
|
|
/// <see cref="ChargenPreviewEntityBuilder.TryBuildBackdrop"/>'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
|
|
/// <c>PrivateEntityViewportRendererDrawOrderTests</c> without needing a
|
|
/// live GPU device or a constructed <see cref="WbDrawDispatcher"/>.
|
|
/// </summary>
|
|
internal static IReadOnlyList<WorldEntity> BuildDrawEntities(WorldEntity? backdrop, WorldEntity main) =>
|
|
backdrop is not null && backdrop.MeshRefs.Count > 0
|
|
? [backdrop, main]
|
|
: [main];
|
|
|
|
/// <summary>
|
|
/// Both retail paperdoll and creature examination call
|
|
/// <c>UIElement_Viewport::SetLight(DISTANT_LIGHT, 2, (0.3,1.9,0.65))</c>.
|
|
/// Byte-decoded confirmation (Batch D re-derivation): the SAME three
|
|
/// float32 constants (<c>0x3e99999a</c>/<c>0x3ff33333</c>/<c>0x3F266666</c>
|
|
/// = 0.3/1.9/0.65) appear verbatim at <c>gmCG3DView::Update</c>'s own
|
|
/// <c>SetLight</c> call site (pseudo-C ~0x004eecd3-0x004eece3) — the
|
|
/// chargen preview uses the EXACT same light this method already ported,
|
|
/// not a different value.
|
|
/// </summary>
|
|
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),
|
|
});
|
|
}
|
|
|
|
private void EnsureRenderTarget(int width, int height)
|
|
{
|
|
if (_target is not null && width == _fbW && height == _fbH)
|
|
return;
|
|
ReleaseRenderTarget();
|
|
|
|
IGpuRenderTarget target;
|
|
try
|
|
{
|
|
target = _device.CreateRenderTarget(new GpuRenderTargetDescription(
|
|
_diagnosticName,
|
|
width,
|
|
height,
|
|
GpuTextureFormat.Rgba8UnormRenderTarget,
|
|
// Depth24Stencil8, as the hand-rolled renderbuffer was: nothing
|
|
// samples it, and the stencil aspect keeps the attachment shape
|
|
// the depth/stencil renderers already expect.
|
|
GpuTextureFormat.Depth24Stencil8,
|
|
SampleCount: 1));
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
Console.WriteLine(
|
|
$"[{_diagnosticName}] render target unavailable ({width}x{height}): {failure.Message}");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// The retained UI blits this attachment as an ordinary table entry.
|
|
// Linear/clamped is the filtering the hand-rolled colour texture set
|
|
// on itself before the §7.1 seam registered it.
|
|
_sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp);
|
|
_slot = _device.RegisterTexture(target.ColorTexture, _sampler);
|
|
}
|
|
catch
|
|
{
|
|
target.Dispose();
|
|
_sampler = null;
|
|
_slot = GpuTextureSlot.Unassigned;
|
|
throw;
|
|
}
|
|
|
|
_target = target;
|
|
_fbW = width;
|
|
_fbH = height;
|
|
}
|
|
|
|
private void ReleaseRenderTarget()
|
|
{
|
|
if (_slot.IsAssigned)
|
|
{
|
|
_device.ReleaseTextureSlot(_slot);
|
|
_slot = GpuTextureSlot.Unassigned;
|
|
}
|
|
_sampler = null;
|
|
_target?.Dispose();
|
|
_target = null;
|
|
_fbW = 0;
|
|
_fbH = 0;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
List<Exception>? failures = null;
|
|
try
|
|
{
|
|
_mainSlot.Dispose();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
try
|
|
{
|
|
_backdropSlot?.Dispose();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
try
|
|
{
|
|
ReleaseRenderTarget();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"The {_diagnosticName} resources did not fully release.",
|
|
failures);
|
|
}
|
|
}
|
|
|
|
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.
|
|
/// </summary>
|
|
private sealed class EntitySlot
|
|
{
|
|
private readonly IWbMeshAdapter _meshAdapter;
|
|
private readonly FixedEntityTextureOwnerLease _textureOwnerLease;
|
|
private readonly string _diagnosticName;
|
|
private readonly List<SyntheticEntityMeshReferenceOwner> _retiringMeshReferences = [];
|
|
|
|
private SyntheticEntityMeshReferenceOwner? _meshReferences;
|
|
|
|
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; }
|
|
|
|
public void Set(WorldEntity? entity)
|
|
{
|
|
ReleaseRetiringMeshReferences();
|
|
|
|
if (ReferenceEquals(Entity, entity))
|
|
return;
|
|
|
|
SyntheticEntityMeshReferenceOwner? replacement = null;
|
|
if (entity is not null)
|
|
{
|
|
replacement = new SyntheticEntityMeshReferenceOwner(
|
|
_meshAdapter,
|
|
CollectMeshIds(entity));
|
|
replacement.Acquire();
|
|
}
|
|
|
|
SyntheticEntityMeshReferenceOwner? previous = _meshReferences;
|
|
try
|
|
{
|
|
_textureOwnerLease.Replace(entity is not null);
|
|
}
|
|
catch (Exception textureFailure)
|
|
{
|
|
if (replacement is null)
|
|
throw;
|
|
|
|
try
|
|
{
|
|
replacement.Dispose();
|
|
}
|
|
catch (Exception rollbackFailure)
|
|
{
|
|
throw new AggregateException(
|
|
$"The {_diagnosticName} texture-owner replacement failed "
|
|
+ "and the replacement mesh-owner rollback did not converge.",
|
|
textureFailure,
|
|
rollbackFailure);
|
|
}
|
|
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
|
.Capture(textureFailure)
|
|
.Throw();
|
|
}
|
|
|
|
_meshReferences = replacement;
|
|
Entity = entity;
|
|
|
|
if (previous is not null)
|
|
{
|
|
try
|
|
{
|
|
previous.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
_retiringMeshReferences.Add(previous);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Entity = null;
|
|
if (_meshReferences is { } current)
|
|
{
|
|
_meshReferences = null;
|
|
_retiringMeshReferences.Add(current);
|
|
}
|
|
|
|
List<Exception>? 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<Exception>? 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);
|
|
}
|
|
}
|
|
}
|
|
}
|