refactor(lifetime): own render resource prefixes
Give terrain, sky, retained UI, portal preparation, and the update/render frame pair explicit single owners. Make shader, texture, text, bindless, and GL construction prefixes checked and retryable so partial failure cannot lose or replay resource ownership. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
fec0d94148
commit
c87b15303d
41 changed files with 3768 additions and 355 deletions
68
src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs
Normal file
68
src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily removes bindless residency around texture-state mutation while
|
||||
/// retaining the obligation to restore the pair across failed mutation or
|
||||
/// failed reacquisition attempts.
|
||||
/// </summary>
|
||||
internal sealed class BindlessTextureMutationGuard(BindlessTexturePair pair)
|
||||
{
|
||||
private bool _restoreRequired;
|
||||
private bool _operationActive;
|
||||
|
||||
internal bool RestoreRequired => _restoreRequired;
|
||||
|
||||
public void Execute(Action mutation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mutation);
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("A bindless texture mutation is already active.");
|
||||
|
||||
_operationActive = true;
|
||||
Exception? mutationFailure = null;
|
||||
Exception? restoreFailure = null;
|
||||
try
|
||||
{
|
||||
_restoreRequired |= pair.HasAnyResident;
|
||||
if (pair.HasAnyResident)
|
||||
pair.Release();
|
||||
|
||||
mutation();
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
mutationFailure = failure;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_restoreRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = pair.Acquire();
|
||||
_restoreRequired = false;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
restoreFailure = failure;
|
||||
}
|
||||
}
|
||||
|
||||
_operationActive = false;
|
||||
}
|
||||
|
||||
if (mutationFailure is not null && restoreFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Texture-state mutation failed and bindless residency could not be restored.",
|
||||
mutationFailure,
|
||||
restoreFailure);
|
||||
}
|
||||
if (mutationFailure is not null)
|
||||
ExceptionDispatchInfo.Capture(mutationFailure).Throw();
|
||||
if (restoreFailure is not null)
|
||||
ExceptionDispatchInfo.Capture(restoreFailure).Throw();
|
||||
}
|
||||
}
|
||||
94
src/AcDream.App/Rendering/BindlessTexturePair.cs
Normal file
94
src/AcDream.App/Rendering/BindlessTexturePair.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the two independent residency edges used by TerrainAtlas. Acquisition
|
||||
/// and release publish each successful edge immediately, so a partial failure
|
||||
/// retries only work that is still pending.
|
||||
/// </summary>
|
||||
internal sealed class BindlessTexturePair(
|
||||
uint firstTexture,
|
||||
uint secondTexture,
|
||||
Func<uint, ulong> acquire,
|
||||
Action<ulong> release)
|
||||
{
|
||||
private ulong _firstHandle;
|
||||
private ulong _secondHandle;
|
||||
private bool _firstResident;
|
||||
private bool _secondResident;
|
||||
|
||||
public bool IsFullyResident => _firstResident && _secondResident;
|
||||
public bool HasAnyResident => _firstResident || _secondResident;
|
||||
|
||||
public (ulong First, ulong Second) Acquire()
|
||||
{
|
||||
if (!_firstResident)
|
||||
{
|
||||
_firstHandle = acquire(firstTexture);
|
||||
_firstResident = true;
|
||||
}
|
||||
|
||||
if (!_secondResident)
|
||||
{
|
||||
try
|
||||
{
|
||||
_secondHandle = acquire(secondTexture);
|
||||
_secondResident = true;
|
||||
}
|
||||
catch (Exception acquisitionFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
Release();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Second bindless texture acquisition failed and the resident prefix did not cleanly roll back.",
|
||||
acquisitionFailure,
|
||||
cleanupFailure);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return (_firstHandle, _secondHandle);
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
if (_firstResident)
|
||||
{
|
||||
try
|
||||
{
|
||||
release(_firstHandle);
|
||||
_firstResident = false;
|
||||
_firstHandle = 0;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (_secondResident)
|
||||
{
|
||||
try
|
||||
{
|
||||
release(_secondHandle);
|
||||
_secondResident = false;
|
||||
_secondHandle = 0;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"One or more bindless texture handles could not be made non-resident.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
67
src/AcDream.App/Rendering/GameFrameGraphSlot.cs
Normal file
67
src/AcDream.App/Rendering/GameFrameGraphSlot.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using AcDream.App.Update;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IGameUpdateFrameRoot
|
||||
{
|
||||
void Tick(UpdateFrameInput input);
|
||||
}
|
||||
|
||||
internal interface IGameRenderFrameRoot
|
||||
{
|
||||
RenderFrameOutcome Render(RenderFrameInput input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sole atomic publication point for the current update/render frame pair.
|
||||
/// Native callbacks are deliberately inert before publication and after
|
||||
/// withdrawal so a partial load or converging shutdown cannot enter half a
|
||||
/// frame graph.
|
||||
/// </summary>
|
||||
internal sealed class GameFrameGraphSlot
|
||||
{
|
||||
private sealed record FrameGraphPair(
|
||||
IGameUpdateFrameRoot Update,
|
||||
IGameRenderFrameRoot Render);
|
||||
|
||||
private FrameGraphPair? _pair;
|
||||
|
||||
public bool IsPublished => Volatile.Read(ref _pair) is not null;
|
||||
|
||||
public void Publish(IGameUpdateFrameRoot update, IGameRenderFrameRoot render)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(update);
|
||||
ArgumentNullException.ThrowIfNull(render);
|
||||
var pair = new FrameGraphPair(update, render);
|
||||
if (Interlocked.CompareExchange(ref _pair, pair, null) is not null)
|
||||
throw new InvalidOperationException("A game frame graph is already published.");
|
||||
}
|
||||
|
||||
public bool Tick(UpdateFrameInput input)
|
||||
{
|
||||
FrameGraphPair? pair = Volatile.Read(ref _pair);
|
||||
if (pair is null)
|
||||
return false;
|
||||
|
||||
pair.Update.Tick(input);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Render(RenderFrameInput input, out RenderFrameOutcome outcome)
|
||||
{
|
||||
FrameGraphPair? pair = Volatile.Read(ref _pair);
|
||||
if (pair is null)
|
||||
{
|
||||
outcome = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
outcome = pair.Render.Render(input);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Withdraw()
|
||||
{
|
||||
Interlocked.Exchange(ref _pair, null);
|
||||
}
|
||||
}
|
||||
21
src/AcDream.App/Rendering/GameRenderResourceLifetime.cs
Normal file
21
src/AcDream.App/Rendering/GameRenderResourceLifetime.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Sole lifetime owner for render resources that are borrowed by, but not
|
||||
/// owned by, their renderers.
|
||||
/// </summary>
|
||||
internal sealed class GameRenderResourceLifetime
|
||||
{
|
||||
private readonly OwnedResourceSlot<TerrainAtlas> _terrainAtlas = new();
|
||||
private readonly OwnedResourceSlot<Shader> _skyShader = new();
|
||||
|
||||
public TerrainAtlas AcquireTerrainAtlas(Func<TerrainAtlas> factory) =>
|
||||
_terrainAtlas.Acquire(factory);
|
||||
|
||||
public Shader AcquireSkyShader(Func<Shader> factory) =>
|
||||
_skyShader.Acquire(factory);
|
||||
|
||||
public void ReleaseTerrainAtlas() => _terrainAtlas.Release();
|
||||
|
||||
public void ReleaseSkyShader() => _skyShader.Release();
|
||||
}
|
||||
|
|
@ -92,8 +92,11 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
|
||||
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
|
||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||
private AcDream.App.Rendering.RenderFrameOrchestrator?
|
||||
_renderFrameOrchestrator;
|
||||
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
||||
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
||||
_renderResourceLifetime = new();
|
||||
private readonly AcDream.App.Rendering.GlConstructionCleanupLedger
|
||||
_glConstructionCleanup = new();
|
||||
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =
|
||||
new(Console.WriteLine);
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
|
|
@ -148,7 +151,6 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
||||
_movementTruthDiagnostics;
|
||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||
private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = null!;
|
||||
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||
// Step 7 projectile presentation. The controller owns no identity map;
|
||||
|
|
@ -202,7 +204,8 @@ public sealed class GameWindow : IDisposable
|
|||
// R1 (render redesign): portal-view draw owners are retained transitively
|
||||
// by the frame orchestrator rather than duplicated as GameWindow roots.
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.PortalTunnelPresentation? _portalTunnel;
|
||||
private readonly AcDream.App.Rendering.TransferableResourceSlot<
|
||||
AcDream.App.Rendering.PortalTunnelPresentation> _portalTunnelFallback = new();
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
|
||||
|
|
@ -342,6 +345,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
||||
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
||||
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
||||
_combatAttackOperations = new();
|
||||
private readonly AcDream.App.Combat.CombatFeedbackSlot
|
||||
|
|
@ -638,7 +642,15 @@ public sealed class GameWindow : IDisposable
|
|||
_displayFramePacing,
|
||||
_hostQuiescence);
|
||||
_windowCallbacks.Attach();
|
||||
_window.Run();
|
||||
try
|
||||
{
|
||||
_window.Run();
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
_glConstructionCleanup.RetainFrom(failure);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoad()
|
||||
|
|
@ -1100,7 +1112,11 @@ public sealed class GameWindow : IDisposable
|
|||
// Build the terrain atlas once from the Region dat. Phase N.5b: the
|
||||
// atlas exposes bindless handles for the modern terrain path, so
|
||||
// BindlessSupport is threaded through.
|
||||
var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats, _bindlessSupport);
|
||||
var terrainAtlas = _renderResourceLifetime.AcquireTerrainAtlas(
|
||||
() => AcDream.App.Rendering.TerrainAtlas.Build(
|
||||
_gl,
|
||||
_dats,
|
||||
_bindlessSupport));
|
||||
// A.5 T22.5: apply anisotropic level from quality preset. Build()
|
||||
// hard-codes 16x; override here to match the resolved quality so Low
|
||||
// (4x) and Medium (8x) actually take effect.
|
||||
|
|
@ -1230,11 +1246,12 @@ public sealed class GameWindow : IDisposable
|
|||
if (_options.RetailUi)
|
||||
{
|
||||
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
|
||||
_uiHost = new AcDream.App.UI.UiHost(
|
||||
_gl,
|
||||
shadersDir,
|
||||
_debugFont,
|
||||
_hostQuiescence);
|
||||
_uiHost = _retailUiLease.AcquireHost(
|
||||
() => new AcDream.App.UI.UiHost(
|
||||
_gl,
|
||||
shadersDir,
|
||||
_debugFont,
|
||||
_hostQuiescence));
|
||||
_retainedInputCapture.Root = _uiHost.Root;
|
||||
_uiHost.Root.UiLocked = _runtimeSettings.Gameplay.LockUI;
|
||||
var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
|
||||
|
|
@ -1360,8 +1377,7 @@ public sealed class GameWindow : IDisposable
|
|||
UiProbeLog);
|
||||
}
|
||||
|
||||
_retailUiRuntime = AcDream.App.UI.RetailUiRuntime.Mount(
|
||||
new AcDream.App.UI.RetailUiRuntimeBindings(
|
||||
var retailUiBindings = new AcDream.App.UI.RetailUiRuntimeBindings(
|
||||
Host: _uiHost,
|
||||
Assets: new AcDream.App.UI.RetailUiAssets(
|
||||
_dats!, _datLock, ResolveChrome, ResolveDatFont,
|
||||
|
|
@ -1511,7 +1527,10 @@ public sealed class GameWindow : IDisposable
|
|||
action => _inputDispatcher?.TryInvokeAutomationAction(action) == true,
|
||||
(action, held) =>
|
||||
_inputDispatcher?.TrySetAutomationActionHeld(action, held) == true,
|
||||
_worldLifecycleAutomation)));
|
||||
_worldLifecycleAutomation));
|
||||
_retailUiRuntime = _retailUiLease.Mount(
|
||||
() => AcDream.App.UI.RetailUiRuntime.CreateUninitialized(
|
||||
retailUiBindings));
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -1966,26 +1985,29 @@ public sealed class GameWindow : IDisposable
|
|||
// T1: invisible portal depth writes (seal/punch) — retail
|
||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
_portalTunnel = AcDream.App.Rendering.PortalTunnelPresentation.CreateRequired(
|
||||
_gl,
|
||||
_dats!,
|
||||
_animLoader!,
|
||||
_hookRouter,
|
||||
_wbDrawDispatcher,
|
||||
_sceneLightingUbo!,
|
||||
_wbMeshAdapter!);
|
||||
_portalTunnelFallback.AcquirePrepared(
|
||||
() => AcDream.App.Rendering.PortalTunnelPresentation.CreateRequired(
|
||||
_gl,
|
||||
_dats!,
|
||||
_animLoader!,
|
||||
_hookRouter,
|
||||
_wbDrawDispatcher,
|
||||
_sceneLightingUbo!,
|
||||
_wbMeshAdapter!),
|
||||
static portalTunnel => portalTunnel.PrepareResources());
|
||||
// Publish the presentation before touching the mesh-reference
|
||||
// backend. Partial acquisition remains reachable by staged
|
||||
// shutdown instead of becoming constructor-local leaked state.
|
||||
_portalTunnel.PrepareResources();
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
// with depth writes off + far plane 1e6 so celestial meshes
|
||||
// never clip. Shares the TextureCache with the static pipeline.
|
||||
var skyShader = new Shader(_gl,
|
||||
Path.Combine(shadersDir, "sky.vert"),
|
||||
Path.Combine(shadersDir, "sky.frag"));
|
||||
var skyShader = _renderResourceLifetime.AcquireSkyShader(
|
||||
() => new Shader(
|
||||
_gl,
|
||||
Path.Combine(shadersDir, "sky.vert"),
|
||||
Path.Combine(shadersDir, "sky.frag")));
|
||||
_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(
|
||||
_gl, _dats, skyShader, _textureCache!, _samplerCache);
|
||||
|
||||
|
|
@ -2422,13 +2444,8 @@ public sealed class GameWindow : IDisposable
|
|||
_localPlayerMode,
|
||||
_playerModeController));
|
||||
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
|
||||
var localTeleportPresentation =
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
||||
_portalTunnel
|
||||
?? throw new InvalidOperationException(
|
||||
"Portal presentation was not composed before local teleport."));
|
||||
_localPlayerTeleport =
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportController(
|
||||
_localPlayerTeleport = _portalTunnelFallback.Transfer(
|
||||
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
|
||||
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
|
||||
_liveEntities,
|
||||
_localPlayerIdentity),
|
||||
|
|
@ -2451,11 +2468,8 @@ public sealed class GameWindow : IDisposable
|
|||
liveSpatialReconciler),
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportSession(
|
||||
_liveSessionController),
|
||||
localTeleportPresentation);
|
||||
// Ownership transferred to LocalPlayerTeleportController. This field
|
||||
// remains only as the staged-startup fallback used by shutdown when
|
||||
// composition fails before the transfer.
|
||||
_portalTunnel = null;
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
||||
portalTunnel)));
|
||||
_localPlayerTeleportSink.Bind(_localPlayerTeleport);
|
||||
var teleportRenderState =
|
||||
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
|
||||
|
|
@ -2652,7 +2666,7 @@ public sealed class GameWindow : IDisposable
|
|||
renderFrameResources,
|
||||
_devToolsFramePresenter,
|
||||
renderWeatherFrame);
|
||||
_renderFrameOrchestrator =
|
||||
var renderFrameOrchestrator =
|
||||
new AcDream.App.Rendering.RenderFrameOrchestrator(
|
||||
_gpuFrameFlights!,
|
||||
framePreparation,
|
||||
|
|
@ -2681,7 +2695,7 @@ public sealed class GameWindow : IDisposable
|
|||
Combat,
|
||||
_selection,
|
||||
_worldSelectionQuery!));
|
||||
_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
||||
_liveEntities),
|
||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||
|
|
@ -2697,6 +2711,7 @@ public sealed class GameWindow : IDisposable
|
|||
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
||||
_playerModeAutoEntry),
|
||||
cameraFrame);
|
||||
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
||||
_liveSessionHost = CreateLiveSessionHost();
|
||||
|
||||
AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm;
|
||||
|
|
@ -3055,8 +3070,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
using var _updStage = _frameProfiler.BeginStage(
|
||||
AcDream.App.Diagnostics.FrameStage.Update);
|
||||
_updateFrameOrchestrator.Tick(
|
||||
new AcDream.App.Update.UpdateFrameInput(dt));
|
||||
_frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt));
|
||||
}
|
||||
|
||||
// Performance overlay state — updated every ~0.5s and written to the
|
||||
|
|
@ -3064,11 +3078,12 @@ public sealed class GameWindow : IDisposable
|
|||
private void OnRender(double deltaSeconds)
|
||||
{
|
||||
Vector2D<int> size = _window!.Size;
|
||||
_renderFrameOrchestrator!.Render(
|
||||
_frameGraphs.Render(
|
||||
new AcDream.App.Rendering.RenderFrameInput(
|
||||
deltaSeconds,
|
||||
size.X,
|
||||
size.Y));
|
||||
size.Y),
|
||||
out _);
|
||||
}
|
||||
|
||||
private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot
|
||||
|
|
@ -3336,7 +3351,7 @@ public sealed class GameWindow : IDisposable
|
|||
new("dispatcher", () => _inputDispatcher?.Deactivate()),
|
||||
new("mouse source", () => _mouseSource?.Deactivate()),
|
||||
new("keyboard source", () => _kbSource?.Deactivate()),
|
||||
new("retained UI input", () => _uiHost?.QuiesceInput()),
|
||||
new("retained UI input", _retailUiLease.QuiesceInput),
|
||||
new("developer tools input", () => _devToolsInputContext?.Deactivate()),
|
||||
]),
|
||||
// Live-session reset retires the complete streaming window. Every
|
||||
|
|
@ -3390,7 +3405,7 @@ public sealed class GameWindow : IDisposable
|
|||
"Gameplay action callback removal remains pending.");
|
||||
_gameplayInputActions = null;
|
||||
}),
|
||||
new("retained UI input", () => _uiHost?.DeactivateInput()),
|
||||
new("retained UI input", _retailUiLease.DeactivateInput),
|
||||
new("developer tools input", () => _devToolsInputContext?.Dispose()),
|
||||
new("camera pointer", () =>
|
||||
{
|
||||
|
|
@ -3444,7 +3459,7 @@ public sealed class GameWindow : IDisposable
|
|||
new("runtime settings targets", _runtimeSettings.UnbindRuntimeTargets),
|
||||
new("world frame composition", () =>
|
||||
{
|
||||
_renderFrameOrchestrator = null;
|
||||
_frameGraphs.Withdraw();
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
|
|
@ -3461,7 +3476,10 @@ public sealed class GameWindow : IDisposable
|
|||
}),
|
||||
new("retail UI", () =>
|
||||
{
|
||||
_retailUiRuntime?.Dispose();
|
||||
_retailUiLease.Dispose();
|
||||
if (!_retailUiLease.IsDisposalComplete)
|
||||
throw new InvalidOperationException(
|
||||
"The retained UI ownership lease did not complete disposal.");
|
||||
_retailUiRuntime = null;
|
||||
_retainedInputCapture.Root = null;
|
||||
_uiHost = null;
|
||||
|
|
@ -3544,8 +3562,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_localPlayerTeleport?.Dispose();
|
||||
_localPlayerTeleport = null;
|
||||
_portalTunnel?.Dispose();
|
||||
_portalTunnel = null;
|
||||
_portalTunnelFallback.ReleaseFallback();
|
||||
}),
|
||||
new("paperdoll viewport", () =>
|
||||
{
|
||||
|
|
@ -3589,6 +3606,15 @@ public sealed class GameWindow : IDisposable
|
|||
new("frame pacing", _displayFramePacing.Dispose),
|
||||
new("frame profiler", _frameProfiler.Dispose),
|
||||
]),
|
||||
new ResourceShutdownStage("dedicated render resources",
|
||||
[
|
||||
new("sky shader", _renderResourceLifetime.ReleaseSkyShader),
|
||||
new("terrain atlas", _renderResourceLifetime.ReleaseTerrainAtlas),
|
||||
]),
|
||||
new ResourceShutdownStage("failed render construction cleanup",
|
||||
[
|
||||
new("GL construction ledger", _glConstructionCleanup.Dispose),
|
||||
]),
|
||||
new ResourceShutdownStage("frame flight owner",
|
||||
[
|
||||
new("frame flights", () =>
|
||||
|
|
|
|||
102
src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs
Normal file
102
src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IRetryableResourceCleanup
|
||||
{
|
||||
bool IsCleanupComplete { get; }
|
||||
void RetryCleanup();
|
||||
}
|
||||
|
||||
internal sealed class GlResourceConstructionException : AggregateException,
|
||||
IRetryableResourceCleanup
|
||||
{
|
||||
private readonly IRetryableResourceCleanup _cleanup;
|
||||
|
||||
public GlResourceConstructionException(
|
||||
string message,
|
||||
IRetryableResourceCleanup cleanup,
|
||||
IEnumerable<Exception> failures)
|
||||
: base(message, failures)
|
||||
{
|
||||
_cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup));
|
||||
}
|
||||
|
||||
public bool IsCleanupComplete => _cleanup.IsCleanupComplete;
|
||||
|
||||
public void RetryCleanup() => _cleanup.RetryCleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lifetime root for cleanup work that could not finish before a throwing GL
|
||||
/// factory returned control. The original exception remains the retry owner;
|
||||
/// this ledger prevents it and its exact pending names from becoming local-only.
|
||||
/// </summary>
|
||||
internal sealed class GlConstructionCleanupLedger : IDisposable
|
||||
{
|
||||
private readonly List<IRetryableResourceCleanup> _pending = [];
|
||||
private bool _disposing;
|
||||
|
||||
public bool IsComplete => _pending.Count == 0;
|
||||
|
||||
public bool RetainFrom(Exception failure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(failure);
|
||||
bool retained = false;
|
||||
Visit(failure);
|
||||
return retained;
|
||||
|
||||
void Visit(Exception current)
|
||||
{
|
||||
if (current is IRetryableResourceCleanup cleanup)
|
||||
{
|
||||
if (!cleanup.IsCleanupComplete && !_pending.Contains(cleanup))
|
||||
_pending.Add(cleanup);
|
||||
retained = true;
|
||||
}
|
||||
|
||||
if (current is AggregateException aggregate)
|
||||
{
|
||||
foreach (Exception inner in aggregate.InnerExceptions)
|
||||
Visit(inner);
|
||||
}
|
||||
else if (current.InnerException is { } inner)
|
||||
{
|
||||
Visit(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposing || _pending.Count == 0)
|
||||
return;
|
||||
|
||||
_disposing = true;
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
for (int i = _pending.Count - 1; i >= 0; i--)
|
||||
{
|
||||
IRetryableResourceCleanup cleanup = _pending[i];
|
||||
try
|
||||
{
|
||||
cleanup.RetryCleanup();
|
||||
if (cleanup.IsCleanupComplete)
|
||||
_pending.RemoveAt(i);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposing = false;
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"One or more failed GL construction transactions remain pending.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
152
src/AcDream.App/Rendering/GlResourceCommand.cs
Normal file
152
src/AcDream.App/Rendering/GlResourceCommand.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Always-on commit boundary for GL resource commands. OpenGL reports ordinary
|
||||
/// command failures through its error flag, so an ownership state machine may
|
||||
/// advance only after the post-command check succeeds.
|
||||
/// </summary>
|
||||
internal static class GlResourceCommand
|
||||
{
|
||||
public static void Execute(GL gl, string context, Action command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(context);
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
command();
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
}
|
||||
|
||||
public static T Execute<T>(GL gl, string context, Func<T> command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(context);
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
T result = command();
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static uint CreateName(
|
||||
GL gl,
|
||||
string resourceName,
|
||||
Func<uint> create,
|
||||
Action<uint> delete)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
|
||||
ArgumentNullException.ThrowIfNull(create);
|
||||
ArgumentNullException.ThrowIfNull(delete);
|
||||
|
||||
return CreateNameCore(
|
||||
resourceName,
|
||||
() => GLHelpers.ThrowOnResourceError(
|
||||
gl,
|
||||
$"create {resourceName} (precondition)"),
|
||||
create,
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"create {resourceName}"),
|
||||
ownedName => Execute(
|
||||
gl,
|
||||
$"rollback {resourceName} name {ownedName}",
|
||||
() => delete(ownedName)));
|
||||
}
|
||||
|
||||
internal static uint CreateNameCore(
|
||||
string resourceName,
|
||||
Action precondition,
|
||||
Func<uint> create,
|
||||
Action postcondition,
|
||||
Action<uint> deleteChecked)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
|
||||
ArgumentNullException.ThrowIfNull(precondition);
|
||||
ArgumentNullException.ThrowIfNull(create);
|
||||
ArgumentNullException.ThrowIfNull(postcondition);
|
||||
ArgumentNullException.ThrowIfNull(deleteChecked);
|
||||
|
||||
uint name = 0;
|
||||
Exception? creationFailure = null;
|
||||
try
|
||||
{
|
||||
precondition();
|
||||
name = create();
|
||||
if (name == 0)
|
||||
throw new InvalidOperationException($"OpenGL returned no {resourceName} name.");
|
||||
postcondition();
|
||||
return name;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
creationFailure = failure;
|
||||
}
|
||||
|
||||
if (name != 0)
|
||||
{
|
||||
var cleanup = new SingleNameCleanup(name, deleteChecked);
|
||||
try
|
||||
{
|
||||
cleanup.RetryCleanup();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
$"Creating {resourceName} failed and its returned GL name could not be released.",
|
||||
cleanup,
|
||||
[creationFailure, cleanupFailure]);
|
||||
}
|
||||
}
|
||||
|
||||
ExceptionDispatchInfo.Capture(creationFailure).Throw();
|
||||
throw new InvalidOperationException("Unreachable GL resource-creation path.");
|
||||
}
|
||||
|
||||
public static uint CreateTexture(GL gl, string context) =>
|
||||
CreateName(gl, context, gl.GenTexture, gl.DeleteTexture);
|
||||
|
||||
public static void DeleteTexture(GL gl, uint texture, string context) =>
|
||||
Execute(gl, context, () => gl.DeleteTexture(texture));
|
||||
|
||||
public static void DeleteShader(GL gl, uint shader, string context) =>
|
||||
Execute(gl, context, () => gl.DeleteShader(shader));
|
||||
|
||||
public static void DeleteProgram(GL gl, uint program, string context) =>
|
||||
Execute(gl, context, () => gl.DeleteProgram(program));
|
||||
|
||||
public static void DeleteBuffer(GL gl, uint buffer, string context) =>
|
||||
Execute(gl, context, () => gl.DeleteBuffer(buffer));
|
||||
|
||||
public static void DeleteVertexArray(GL gl, uint vertexArray, string context) =>
|
||||
Execute(gl, context, () => gl.DeleteVertexArray(vertexArray));
|
||||
|
||||
private sealed class SingleNameCleanup(uint name, Action<uint> delete)
|
||||
: IRetryableResourceCleanup
|
||||
{
|
||||
private uint _name = name;
|
||||
private bool _running;
|
||||
|
||||
public bool IsCleanupComplete => _name == 0;
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
if (_running || _name == 0)
|
||||
return;
|
||||
|
||||
_running = true;
|
||||
try
|
||||
{
|
||||
delete(_name);
|
||||
_name = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IGlTextureNameApi
|
||||
{
|
||||
uint GenTexture();
|
||||
void DeleteTexture(uint texture);
|
||||
}
|
||||
|
||||
internal sealed class GlTextureNameApi(Silk.NET.OpenGL.GL gl) : IGlTextureNameApi
|
||||
{
|
||||
public uint GenTexture() =>
|
||||
GlResourceCommand.CreateTexture(gl, "terrain construction texture");
|
||||
|
||||
public void DeleteTexture(uint texture) =>
|
||||
GlResourceCommand.DeleteTexture(
|
||||
gl,
|
||||
texture,
|
||||
$"delete terrain construction texture {texture}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retains every texture name immediately after allocation until the complete
|
||||
/// aggregate owner has been constructed. A failed factory rolls names back in
|
||||
/// reverse acquisition order and attempts every deletion.
|
||||
/// </summary>
|
||||
internal sealed class GlTextureConstructionTransaction(IGlTextureNameApi api)
|
||||
: IRetryableResourceCleanup
|
||||
{
|
||||
private readonly List<uint> _ownedNames = [];
|
||||
private bool _finished;
|
||||
|
||||
public bool IsCleanupComplete => _finished;
|
||||
|
||||
public uint Allocate()
|
||||
{
|
||||
if (_finished)
|
||||
throw new InvalidOperationException("The texture construction transaction is finished.");
|
||||
|
||||
uint texture = api.GenTexture();
|
||||
if (texture == 0)
|
||||
throw new InvalidOperationException("OpenGL returned no texture name.");
|
||||
_ownedNames.Add(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
if (_finished)
|
||||
throw new InvalidOperationException("The texture construction transaction is finished.");
|
||||
_ownedNames.Clear();
|
||||
_finished = true;
|
||||
}
|
||||
|
||||
public void Rollback()
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
List<Exception>? failures = null;
|
||||
for (int i = _ownedNames.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint texture = _ownedNames[i];
|
||||
try
|
||||
{
|
||||
api.DeleteTexture(texture);
|
||||
_ownedNames.RemoveAt(i);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Texture construction rollback could not delete OpenGL name {texture}.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"Texture construction rollback did not release every allocated OpenGL name.",
|
||||
failures);
|
||||
|
||||
_finished = true;
|
||||
}
|
||||
|
||||
public void RetryCleanup() => Rollback();
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ internal sealed class NullRenderFrameFailureRecovery : IRenderFrameFailureRecove
|
|||
/// and close failure are reported together, while either failure alone
|
||||
/// propagates directly.
|
||||
/// </remarks>
|
||||
internal sealed class RenderFrameOrchestrator
|
||||
internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
||||
{
|
||||
private readonly IRenderFrameLifetime _lifetime;
|
||||
private readonly IRenderFrameResourcePhase _resources;
|
||||
|
|
|
|||
63
src/AcDream.App/Rendering/ResourceCleanupGroup.cs
Normal file
63
src/AcDream.App/Rendering/ResourceCleanupGroup.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Reverse-order, all-attempted cleanup owner used while a composite resource
|
||||
/// is still under construction and after it becomes the aggregate owner.
|
||||
/// </summary>
|
||||
internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
|
||||
{
|
||||
private sealed record Entry(string Name, Action Release)
|
||||
{
|
||||
public bool Complete { get; set; }
|
||||
}
|
||||
|
||||
private readonly List<Entry> _entries = [];
|
||||
private bool _running;
|
||||
|
||||
public bool IsCleanupComplete => _entries.All(static entry => entry.Complete);
|
||||
|
||||
public void Add(string name, Action release)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
ArgumentNullException.ThrowIfNull(release);
|
||||
if (_running || IsCleanupComplete && _entries.Count != 0)
|
||||
throw new InvalidOperationException("The resource cleanup group is no longer accepting ownership.");
|
||||
_entries.Add(new Entry(name, release));
|
||||
}
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
if (_running || IsCleanupComplete)
|
||||
return;
|
||||
|
||||
_running = true;
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
for (int i = _entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Entry entry = _entries[i];
|
||||
if (entry.Complete)
|
||||
continue;
|
||||
try
|
||||
{
|
||||
entry.Release();
|
||||
entry.Complete = true;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Resource cleanup operation '{entry.Name}' failed.",
|
||||
failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_running = false;
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException("Composite resource cleanup remains incomplete.", failures);
|
||||
}
|
||||
}
|
||||
83
src/AcDream.App/Rendering/RestoredTextureBindingMutation.cs
Normal file
83
src/AcDream.App/Rendering/RestoredTextureBindingMutation.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Runs a texture mutation with the caller's prior binding restored on every
|
||||
/// exit path. The concrete GL adapter supplies checked commands; tests can
|
||||
/// fault each edge without requiring a graphics context.
|
||||
/// </summary>
|
||||
internal sealed class RestoredTextureBindingMutation
|
||||
{
|
||||
private uint? _pendingRestore;
|
||||
private bool _operationActive;
|
||||
|
||||
internal bool HasPendingRestore => _pendingRestore.HasValue;
|
||||
|
||||
public void Execute(
|
||||
Func<uint> readBinding,
|
||||
Action<uint> bind,
|
||||
uint mutationBinding,
|
||||
Action mutation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(readBinding);
|
||||
ArgumentNullException.ThrowIfNull(bind);
|
||||
ArgumentNullException.ThrowIfNull(mutation);
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("A texture-binding mutation is already active.");
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
ExecuteCore(readBinding, bind, mutationBinding, mutation);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCore(
|
||||
Func<uint> readBinding,
|
||||
Action<uint> bind,
|
||||
uint mutationBinding,
|
||||
Action mutation)
|
||||
{
|
||||
if (_pendingRestore is uint pending)
|
||||
{
|
||||
bind(pending);
|
||||
_pendingRestore = null;
|
||||
}
|
||||
|
||||
uint previousBinding = readBinding();
|
||||
_pendingRestore = previousBinding;
|
||||
Exception? mutationFailure = null;
|
||||
try
|
||||
{
|
||||
bind(mutationBinding);
|
||||
mutation();
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
mutationFailure = failure;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bind(previousBinding);
|
||||
_pendingRestore = null;
|
||||
}
|
||||
catch (Exception restoreFailure)
|
||||
{
|
||||
if (mutationFailure is not null)
|
||||
throw new AggregateException(
|
||||
"Texture mutation and binding restoration both failed.",
|
||||
mutationFailure,
|
||||
restoreFailure);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (mutationFailure is not null)
|
||||
ExceptionDispatchInfo.Capture(mutationFailure).Throw();
|
||||
}
|
||||
}
|
||||
171
src/AcDream.App/Rendering/RuntimeResourceSlots.cs
Normal file
171
src/AcDream.App/Rendering/RuntimeResourceSlots.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>Retryable one-owner slot for a disposable runtime resource.</summary>
|
||||
internal sealed class OwnedResourceSlot<T>
|
||||
where T : class, IDisposable
|
||||
{
|
||||
private T? _resource;
|
||||
private bool _operationActive;
|
||||
|
||||
public bool HasResource => _resource is not null;
|
||||
|
||||
public T Acquire(Func<T> factory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
if (_operationActive || _resource is not null)
|
||||
throw new InvalidOperationException($"A {typeof(T).Name} is already owned.");
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
T resource = factory()
|
||||
?? throw new InvalidOperationException($"The {typeof(T).Name} factory returned null.");
|
||||
_resource = resource;
|
||||
return resource;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public T Borrow()
|
||||
{
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException($"The {typeof(T).Name} owner is changing state.");
|
||||
return _resource
|
||||
?? throw new InvalidOperationException($"No {typeof(T).Name} is currently owned.");
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
T? resource = _resource;
|
||||
if (resource is null)
|
||||
return;
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException($"The {typeof(T).Name} owner is changing state.");
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
resource.Dispose();
|
||||
if (ReferenceEquals(_resource, resource))
|
||||
_resource = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void TransferOwnership(T expected)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expected);
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException($"The {typeof(T).Name} owner is changing state.");
|
||||
if (!ReferenceEquals(_resource, expected))
|
||||
throw new InvalidOperationException(
|
||||
$"The expected {typeof(T).Name} is not owned by this slot.");
|
||||
_resource = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns a fallback resource until a complete destination owner factory
|
||||
/// returns. A throwing factory leaves fallback ownership unchanged.
|
||||
/// </summary>
|
||||
internal sealed class TransferableResourceSlot<T>
|
||||
where T : class, IDisposable
|
||||
{
|
||||
private readonly OwnedResourceSlot<T> _fallback = new();
|
||||
private bool _operationActive;
|
||||
private bool _prepared;
|
||||
|
||||
public bool HasFallback => _fallback.HasResource;
|
||||
|
||||
public T Acquire(Func<T> factory) => Execute(() =>
|
||||
{
|
||||
T resource = _fallback.Acquire(factory);
|
||||
_prepared = true;
|
||||
return resource;
|
||||
});
|
||||
|
||||
public T AcquirePrepared(Func<T> factory, Action<T> prepare) => Execute(() =>
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
ArgumentNullException.ThrowIfNull(prepare);
|
||||
|
||||
T resource;
|
||||
if (_fallback.HasResource)
|
||||
{
|
||||
resource = _fallback.Borrow();
|
||||
}
|
||||
else
|
||||
{
|
||||
resource = _fallback.Acquire(factory);
|
||||
_prepared = false;
|
||||
}
|
||||
|
||||
if (!_prepared)
|
||||
{
|
||||
prepare(resource);
|
||||
_prepared = true;
|
||||
}
|
||||
|
||||
return resource;
|
||||
});
|
||||
|
||||
public T Borrow()
|
||||
{
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The transferable resource owner is changing state.");
|
||||
return _fallback.Borrow();
|
||||
}
|
||||
|
||||
public TOwner Transfer<TOwner>(Func<T, TOwner> destinationFactory)
|
||||
where TOwner : class
|
||||
{
|
||||
return Execute(() =>
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(destinationFactory);
|
||||
if (!_prepared)
|
||||
throw new InvalidOperationException(
|
||||
"The transferable resource has not completed preparation.");
|
||||
T resource = _fallback.Borrow();
|
||||
TOwner owner = destinationFactory(resource)
|
||||
?? throw new InvalidOperationException("The destination owner factory returned null.");
|
||||
|
||||
// The destination factory contract transfers ownership of resource.
|
||||
// Clear without disposing only after the complete owner exists.
|
||||
_fallback.TransferOwnership(resource);
|
||||
_prepared = false;
|
||||
return owner;
|
||||
});
|
||||
}
|
||||
|
||||
public void ReleaseFallback() => Execute(
|
||||
() =>
|
||||
{
|
||||
_fallback.Release();
|
||||
if (!_fallback.HasResource)
|
||||
_prepared = false;
|
||||
return true;
|
||||
});
|
||||
|
||||
private TResult Execute<TResult>(Func<TResult> operation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The transferable resource owner is changing state.");
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
return operation();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,36 +7,17 @@ public sealed class Shader : IDisposable
|
|||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Dictionary<string, int> _uniformLocations = new(StringComparer.Ordinal);
|
||||
public uint Program { get; }
|
||||
public uint Program { get; private set; }
|
||||
|
||||
public Shader(GL gl, string vertexPath, string fragmentPath)
|
||||
{
|
||||
_gl = gl;
|
||||
uint vert = Compile(File.ReadAllText(vertexPath), ShaderType.VertexShader);
|
||||
uint frag = Compile(File.ReadAllText(fragmentPath), ShaderType.FragmentShader);
|
||||
|
||||
Program = _gl.CreateProgram();
|
||||
_gl.AttachShader(Program, vert);
|
||||
_gl.AttachShader(Program, frag);
|
||||
_gl.LinkProgram(Program);
|
||||
_gl.GetProgram(Program, ProgramPropertyARB.LinkStatus, out int linked);
|
||||
if (linked == 0)
|
||||
throw new Exception("program link failed: " + _gl.GetProgramInfoLog(Program));
|
||||
_gl.DetachShader(Program, vert);
|
||||
_gl.DetachShader(Program, frag);
|
||||
_gl.DeleteShader(vert);
|
||||
_gl.DeleteShader(frag);
|
||||
}
|
||||
|
||||
private uint Compile(string source, ShaderType type)
|
||||
{
|
||||
uint id = _gl.CreateShader(type);
|
||||
_gl.ShaderSource(id, source);
|
||||
_gl.CompileShader(id);
|
||||
_gl.GetShader(id, ShaderParameterName.CompileStatus, out int ok);
|
||||
if (ok == 0)
|
||||
throw new Exception($"{type} compile failed: " + _gl.GetShaderInfoLog(id));
|
||||
return id;
|
||||
string vertexSource = File.ReadAllText(vertexPath);
|
||||
string fragmentSource = File.ReadAllText(fragmentPath);
|
||||
Program = ShaderProgramConstruction.Build(
|
||||
new GlShaderProgramBuildApi(gl),
|
||||
vertexSource,
|
||||
fragmentSource);
|
||||
}
|
||||
|
||||
public void Use() => _gl.UseProgram(Program);
|
||||
|
|
@ -90,7 +71,12 @@ public sealed class Shader : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
uint program = Program;
|
||||
if (program == 0)
|
||||
return;
|
||||
|
||||
_uniformLocations.Clear();
|
||||
_gl.DeleteProgram(Program);
|
||||
GlResourceCommand.DeleteProgram(_gl, program, $"delete Shader program {program}");
|
||||
Program = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
325
src/AcDream.App/Rendering/ShaderProgramConstruction.cs
Normal file
325
src/AcDream.App/Rendering/ShaderProgramConstruction.cs
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IShaderProgramBuildApi
|
||||
{
|
||||
uint CreateShader(ShaderType type);
|
||||
void ShaderSource(uint shader, string source);
|
||||
void CompileShader(uint shader);
|
||||
int GetShaderCompileStatus(uint shader);
|
||||
string GetShaderInfoLog(uint shader);
|
||||
uint CreateProgram();
|
||||
void AttachShader(uint program, uint shader);
|
||||
void LinkProgram(uint program);
|
||||
int GetProgramLinkStatus(uint program);
|
||||
string GetProgramInfoLog(uint program);
|
||||
void DetachShader(uint program, uint shader);
|
||||
void DeleteShader(uint shader);
|
||||
void DeleteProgram(uint program);
|
||||
}
|
||||
|
||||
internal sealed class GlShaderProgramBuildApi(GL gl) : IShaderProgramBuildApi
|
||||
{
|
||||
public uint CreateShader(ShaderType type) =>
|
||||
GlResourceCommand.CreateName(
|
||||
gl,
|
||||
$"{type} shader",
|
||||
() => gl.CreateShader(type),
|
||||
gl.DeleteShader);
|
||||
|
||||
public void ShaderSource(uint shader, string source) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"upload shader source {shader}",
|
||||
() => gl.ShaderSource(shader, source));
|
||||
|
||||
public void CompileShader(uint shader) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"compile shader {shader}",
|
||||
() => gl.CompileShader(shader));
|
||||
|
||||
public void GetShader(uint shader, ShaderParameterName name, out int value) =>
|
||||
gl.GetShader(shader, name, out value);
|
||||
|
||||
public int GetShaderCompileStatus(uint shader)
|
||||
{
|
||||
return GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"read shader {shader} compile status",
|
||||
() =>
|
||||
{
|
||||
gl.GetShader(shader, ShaderParameterName.CompileStatus, out int value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
public string GetShaderInfoLog(uint shader) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"read shader {shader} info log",
|
||||
() => gl.GetShaderInfoLog(shader));
|
||||
|
||||
public uint CreateProgram() =>
|
||||
GlResourceCommand.CreateName(
|
||||
gl,
|
||||
"shader program",
|
||||
gl.CreateProgram,
|
||||
gl.DeleteProgram);
|
||||
|
||||
public void AttachShader(uint program, uint shader) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"attach shader {shader} to program {program}",
|
||||
() => gl.AttachShader(program, shader));
|
||||
|
||||
public void LinkProgram(uint program) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"link shader program {program}",
|
||||
() => gl.LinkProgram(program));
|
||||
|
||||
public int GetProgramLinkStatus(uint program)
|
||||
{
|
||||
return GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"read shader program {program} link status",
|
||||
() =>
|
||||
{
|
||||
gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
public string GetProgramInfoLog(uint program) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"read shader program {program} info log",
|
||||
() => gl.GetProgramInfoLog(program));
|
||||
|
||||
public void DetachShader(uint program, uint shader) =>
|
||||
GlResourceCommand.Execute(
|
||||
gl,
|
||||
$"detach shader {shader} from program {program}",
|
||||
() => gl.DetachShader(program, shader));
|
||||
|
||||
public void DeleteShader(uint shader) =>
|
||||
GlResourceCommand.DeleteShader(gl, shader, $"delete shader {shader}");
|
||||
|
||||
public void DeleteProgram(uint program) =>
|
||||
GlResourceCommand.DeleteProgram(gl, program, $"delete shader program {program}");
|
||||
}
|
||||
|
||||
internal sealed class ShaderProgramCleanup(IShaderProgramBuildApi api)
|
||||
: IRetryableResourceCleanup
|
||||
{
|
||||
public uint Vertex { get; set; }
|
||||
public uint Fragment { get; set; }
|
||||
public uint Program { get; set; }
|
||||
public bool VertexAttached { get; set; }
|
||||
public bool FragmentAttached { get; set; }
|
||||
|
||||
public bool IsCleanupComplete => Vertex == 0 && Fragment == 0 && Program == 0;
|
||||
|
||||
public uint TransferProgram()
|
||||
{
|
||||
if (Vertex != 0 || Fragment != 0 || Program == 0)
|
||||
throw new InvalidOperationException("The linked program is not ready for ownership transfer.");
|
||||
uint program = Program;
|
||||
Program = 0;
|
||||
return program;
|
||||
}
|
||||
|
||||
public List<Exception> Release(bool includeProgram)
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Program != 0 && Vertex != 0 && VertexAttached)
|
||||
{
|
||||
api.DetachShader(Program, Vertex);
|
||||
VertexAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Program != 0 && Fragment != 0 && FragmentAttached)
|
||||
{
|
||||
api.DetachShader(Program, Fragment);
|
||||
FragmentAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Vertex != 0)
|
||||
{
|
||||
api.DeleteShader(Vertex);
|
||||
Vertex = 0;
|
||||
VertexAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Fragment != 0)
|
||||
{
|
||||
api.DeleteShader(Fragment);
|
||||
Fragment = 0;
|
||||
FragmentAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
if (includeProgram)
|
||||
{
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Program != 0)
|
||||
{
|
||||
api.DeleteProgram(Program);
|
||||
Program = 0;
|
||||
VertexAttached = false;
|
||||
FragmentAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
public List<Exception> ReleaseProgramOnly()
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Program != 0)
|
||||
{
|
||||
api.DeleteProgram(Program);
|
||||
Program = 0;
|
||||
VertexAttached = false;
|
||||
FragmentAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
return failures;
|
||||
}
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
List<Exception> failures = Release(includeProgram: true);
|
||||
if (!IsCleanupComplete)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Shader program cleanup remains incomplete.",
|
||||
failures.Count == 0
|
||||
? [new InvalidOperationException("Shader cleanup retained names without reporting an operation failure.")]
|
||||
: failures);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Try(Action operation, List<Exception> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
operation();
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
failures.Add(failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ShaderProgramConstruction
|
||||
{
|
||||
public static uint Build(
|
||||
IShaderProgramBuildApi api,
|
||||
string vertexSource,
|
||||
string fragmentSource)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(api);
|
||||
ArgumentNullException.ThrowIfNull(vertexSource);
|
||||
ArgumentNullException.ThrowIfNull(fragmentSource);
|
||||
|
||||
var cleanup = new ShaderProgramCleanup(api);
|
||||
Exception? constructionFailure = null;
|
||||
|
||||
try
|
||||
{
|
||||
cleanup.Vertex = CreateShader(api, ShaderType.VertexShader);
|
||||
Compile(api, cleanup.Vertex, ShaderType.VertexShader, vertexSource);
|
||||
cleanup.Fragment = CreateShader(api, ShaderType.FragmentShader);
|
||||
Compile(api, cleanup.Fragment, ShaderType.FragmentShader, fragmentSource);
|
||||
cleanup.Program = api.CreateProgram();
|
||||
if (cleanup.Program == 0)
|
||||
throw new InvalidOperationException("OpenGL returned no shader program name.");
|
||||
|
||||
api.AttachShader(cleanup.Program, cleanup.Vertex);
|
||||
cleanup.VertexAttached = true;
|
||||
api.AttachShader(cleanup.Program, cleanup.Fragment);
|
||||
cleanup.FragmentAttached = true;
|
||||
api.LinkProgram(cleanup.Program);
|
||||
if (api.GetProgramLinkStatus(cleanup.Program) == 0)
|
||||
throw new InvalidOperationException(
|
||||
"program link failed: " + api.GetProgramInfoLog(cleanup.Program));
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
constructionFailure = failure;
|
||||
}
|
||||
|
||||
List<Exception> cleanupFailures = cleanup.Release(includeProgram: false);
|
||||
|
||||
if (constructionFailure is null && cleanupFailures.Count == 0)
|
||||
return cleanup.TransferProgram();
|
||||
|
||||
cleanupFailures.AddRange(cleanup.ReleaseProgramOnly());
|
||||
|
||||
if (constructionFailure is not null && cleanupFailures.Count == 0)
|
||||
ExceptionDispatchInfo.Capture(constructionFailure).Throw();
|
||||
|
||||
var failures = new List<Exception>(cleanupFailures.Count + 1);
|
||||
if (constructionFailure is not null)
|
||||
failures.Add(constructionFailure);
|
||||
failures.AddRange(cleanupFailures);
|
||||
const string message =
|
||||
"Shader program construction failed and one or more temporary OpenGL names could not be cleanly released.";
|
||||
if (!cleanup.IsCleanupComplete)
|
||||
throw new GlResourceConstructionException(message, cleanup, failures);
|
||||
throw new AggregateException(message, failures);
|
||||
}
|
||||
|
||||
private static uint CreateShader(
|
||||
IShaderProgramBuildApi api,
|
||||
ShaderType type)
|
||||
{
|
||||
uint shader = api.CreateShader(type);
|
||||
if (shader == 0)
|
||||
throw new InvalidOperationException($"OpenGL returned no {type} name.");
|
||||
return shader;
|
||||
}
|
||||
|
||||
private static void Compile(
|
||||
IShaderProgramBuildApi api,
|
||||
uint shader,
|
||||
ShaderType type,
|
||||
string source)
|
||||
{
|
||||
api.ShaderSource(shader, source);
|
||||
api.CompileShader(shader);
|
||||
if (api.GetShaderCompileStatus(shader) == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"{type} compile failed: " + api.GetShaderInfoLog(shader));
|
||||
}
|
||||
}
|
||||
|
|
@ -61,12 +61,10 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
public IReadOnlyList<uint> RoadAlphaRCodes { get; }
|
||||
|
||||
private readonly Wb.BindlessSupport? _bindless;
|
||||
|
||||
// Cached bindless handles. Generated lazily on first GetBindlessHandles() call;
|
||||
// reused for the lifetime of the atlas.
|
||||
private ulong _terrainHandle;
|
||||
private ulong _alphaHandle;
|
||||
private bool _handlesGenerated;
|
||||
private readonly BindlessTexturePair? _bindlessHandles;
|
||||
private readonly BindlessTextureMutationGuard? _bindlessMutation;
|
||||
private readonly RestoredTextureBindingMutation _anisotropyBindingMutation = new();
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
|
||||
/// <summary>
|
||||
/// Get 64-bit bindless handles for the terrain + alpha texture arrays.
|
||||
|
|
@ -80,13 +78,8 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
if (_bindless is null)
|
||||
throw new InvalidOperationException(
|
||||
"TerrainAtlas was constructed without BindlessSupport; cannot return bindless handles.");
|
||||
if (!_handlesGenerated)
|
||||
{
|
||||
_terrainHandle = _bindless.GetResidentHandle(GlTexture);
|
||||
_alphaHandle = _bindless.GetResidentHandle(GlAlphaTexture);
|
||||
_handlesGenerated = true;
|
||||
}
|
||||
return (_terrainHandle, _alphaHandle);
|
||||
(ulong terrain, ulong alpha) = _bindlessHandles!.Acquire();
|
||||
return (terrain, alpha);
|
||||
}
|
||||
|
||||
private TerrainAtlas(
|
||||
|
|
@ -112,6 +105,15 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
CornerAlphaTCodes = cornerTCodes;
|
||||
SideAlphaTCodes = sideTCodes;
|
||||
RoadAlphaRCodes = roadRCodes;
|
||||
if (bindless is not null)
|
||||
{
|
||||
_bindlessHandles = new BindlessTexturePair(
|
||||
glTexture,
|
||||
glAlphaTexture,
|
||||
bindless.GetResidentHandle,
|
||||
bindless.MakeNonResident);
|
||||
_bindlessMutation = new BindlessTextureMutationGuard(_bindlessHandles);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -120,6 +122,37 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
/// to RGBA8, and uploading as layers in a single GL_TEXTURE_2D_ARRAY.
|
||||
/// </summary>
|
||||
public static TerrainAtlas Build(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||
{
|
||||
var textures = new GlTextureConstructionTransaction(new GlTextureNameApi(gl));
|
||||
try
|
||||
{
|
||||
TerrainAtlas atlas = BuildCore(gl, dats, bindless, textures);
|
||||
textures.Commit();
|
||||
return atlas;
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
textures.Rollback();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
"TerrainAtlas construction failed and its allocated OpenGL texture names did not cleanly roll back.",
|
||||
textures,
|
||||
[constructionFailure, cleanupFailure]);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static TerrainAtlas BuildCore(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
Wb.BindlessSupport? bindless,
|
||||
GlTextureConstructionTransaction textures)
|
||||
{
|
||||
var region = dats.Get<Region>(0x13000000u)
|
||||
?? throw new InvalidOperationException("Region dat id 0x13000000 missing");
|
||||
|
|
@ -129,7 +162,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
if (terrainDesc is null || terrainDesc.Count == 0)
|
||||
{
|
||||
Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer");
|
||||
return BuildFallback(gl, bindless);
|
||||
return BuildFallback(gl, bindless, textures);
|
||||
}
|
||||
|
||||
// ---- Terrain atlas (unchanged Phase 2b logic) ----
|
||||
|
|
@ -179,29 +212,45 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
}
|
||||
|
||||
int layerCount = decodedByType.Count;
|
||||
uint tex = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
|
||||
(uint)maxW, (uint)maxH, (uint)layerCount,
|
||||
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
|
||||
var map = new Dictionary<uint, uint>();
|
||||
int layerIdx = 0;
|
||||
foreach (var kvp in decodedByType)
|
||||
uint tex = TrackedTextureConstruction.Create(
|
||||
textures,
|
||||
gl,
|
||||
"upload terrain atlas texture array",
|
||||
texture =>
|
||||
{
|
||||
byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH);
|
||||
fixed (byte* p = buffer)
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, texture);
|
||||
gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
|
||||
(uint)maxW, (uint)maxH, (uint)layerCount,
|
||||
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
|
||||
int layerIdx = 0;
|
||||
foreach (var kvp in decodedByType)
|
||||
{
|
||||
gl.TexSubImage3D(
|
||||
TextureTarget.Texture2DArray, 0,
|
||||
0, 0, layerIdx,
|
||||
(uint)maxW, (uint)maxH, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH);
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
gl.TexSubImage3D(
|
||||
TextureTarget.Texture2DArray, 0,
|
||||
0, 0, layerIdx,
|
||||
(uint)maxW, (uint)maxH, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
}
|
||||
map[kvp.Key] = (uint)layerIdx;
|
||||
layerIdx++;
|
||||
}
|
||||
map[kvp.Key] = (uint)layerIdx;
|
||||
layerIdx++;
|
||||
}
|
||||
|
||||
// A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality.
|
||||
gl.GenerateMipmap(TextureTarget.Texture2DArray);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
// GL_TEXTURE_MAX_ANISOTROPY = 0x84FE (GL_EXT_texture_filter_anisotropic / ARB_texture_filter_anisotropic).
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, 16.0f);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
});
|
||||
|
||||
var tilingByLayer = TerrainTextureTilingTable.Build(
|
||||
map.Select(entry =>
|
||||
|
|
@ -209,22 +258,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
? repeatCount
|
||||
: 1u)));
|
||||
|
||||
// A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality.
|
||||
gl.GenerateMipmap(TextureTarget.Texture2DArray);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
// GL_TEXTURE_MAX_ANISOTROPY = 0x84FE (GL_EXT_texture_filter_anisotropic / ARB_texture_filter_anisotropic).
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, 16.0f);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
|
||||
Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} (mipmaps+aniso16x)");
|
||||
|
||||
// ---- Alpha atlas (new in Phase 3c.2) ----
|
||||
// texMerge is guaranteed non-null here: the early return above exited
|
||||
// if texMerge?.TerrainDesc was null.
|
||||
var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!);
|
||||
var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!, textures);
|
||||
|
||||
return new TerrainAtlas(
|
||||
gl,
|
||||
|
|
@ -252,7 +291,10 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> roadRCodes);
|
||||
|
||||
private static AlphaAtlasBuildResult BuildAlphaAtlas(
|
||||
GL gl, IDatReaderWriter dats, DatReaderWriter.Types.TexMerge texMerge)
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
DatReaderWriter.Types.TexMerge texMerge,
|
||||
GlTextureConstructionTransaction textures)
|
||||
{
|
||||
var decoded = new List<DecodedTexture>();
|
||||
var cornerLayers = new List<byte>();
|
||||
|
|
@ -305,15 +347,21 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
if (decoded.Count == 0)
|
||||
{
|
||||
Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback");
|
||||
uint fallbackAlpha = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, fallbackAlpha);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
uint fallbackAlpha = TrackedTextureConstruction.Create(
|
||||
textures,
|
||||
gl,
|
||||
"upload fallback terrain alpha texture array",
|
||||
texture =>
|
||||
{
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, texture);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
});
|
||||
return new AlphaAtlasBuildResult(
|
||||
fallbackAlpha, 1,
|
||||
cornerLayers, sideLayers, roadLayers,
|
||||
|
|
@ -329,31 +377,37 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
if (d.Height > aMaxH) aMaxH = d.Height;
|
||||
}
|
||||
|
||||
uint glAlpha = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, glAlpha);
|
||||
gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
|
||||
(uint)aMaxW, (uint)aMaxH, (uint)decoded.Count,
|
||||
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
|
||||
for (int i = 0; i < decoded.Count; i++)
|
||||
uint glAlpha = TrackedTextureConstruction.Create(
|
||||
textures,
|
||||
gl,
|
||||
"upload terrain alpha texture array",
|
||||
texture =>
|
||||
{
|
||||
var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH);
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
gl.TexSubImage3D(
|
||||
TextureTarget.Texture2DArray, 0,
|
||||
0, 0, i,
|
||||
(uint)aMaxW, (uint)aMaxH, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
}
|
||||
}
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, texture);
|
||||
gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
|
||||
(uint)aMaxW, (uint)aMaxH, (uint)decoded.Count,
|
||||
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
for (int i = 0; i < decoded.Count; i++)
|
||||
{
|
||||
var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH);
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
gl.TexSubImage3D(
|
||||
TextureTarget.Texture2DArray, 0,
|
||||
0, 0, i,
|
||||
(uint)aMaxW, (uint)aMaxH, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
}
|
||||
}
|
||||
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
});
|
||||
|
||||
Console.WriteLine(
|
||||
$"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} "
|
||||
|
|
@ -412,25 +466,40 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
return dst;
|
||||
}
|
||||
|
||||
private static TerrainAtlas BuildFallback(GL gl, Wb.BindlessSupport? bindless = null)
|
||||
private static TerrainAtlas BuildFallback(
|
||||
GL gl,
|
||||
Wb.BindlessSupport? bindless,
|
||||
GlTextureConstructionTransaction textures)
|
||||
{
|
||||
uint tex = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
uint tex = TrackedTextureConstruction.Create(
|
||||
textures,
|
||||
gl,
|
||||
"upload fallback terrain texture array",
|
||||
texture =>
|
||||
{
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, texture);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
});
|
||||
|
||||
// Fallback alpha atlas: 1x1 white, no layers tracked
|
||||
uint alphaTex = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, alphaTex);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
uint alphaTex = TrackedTextureConstruction.Create(
|
||||
textures,
|
||||
gl,
|
||||
"upload fallback alpha texture array",
|
||||
texture =>
|
||||
{
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, texture);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
});
|
||||
|
||||
return new TerrainAtlas(
|
||||
gl,
|
||||
|
|
@ -453,46 +522,65 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
/// </summary>
|
||||
public void SetAnisotropic(int level)
|
||||
{
|
||||
// If bindless handles are live we must make them non-resident before
|
||||
// mutating texture state, then re-resident after.
|
||||
bool wasResident = _handlesGenerated && _bindless is not null;
|
||||
if (wasResident)
|
||||
void Mutate()
|
||||
{
|
||||
_bindless!.MakeNonResident(_terrainHandle);
|
||||
// Alpha texture is not affected by anisotropic but we must keep
|
||||
// residency symmetric — re-generate both handles after.
|
||||
_bindless.MakeNonResident(_alphaHandle);
|
||||
_handlesGenerated = false;
|
||||
_anisotropyBindingMutation.Execute(
|
||||
() => unchecked((uint)GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"read terrain-array binding before anisotropy mutation",
|
||||
() => _gl.GetInteger(GetPName.TextureBinding2DArray))),
|
||||
binding => GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"set terrain-array binding for anisotropy mutation",
|
||||
() => _gl.BindTexture(TextureTarget.Texture2DArray, binding)),
|
||||
GlTexture,
|
||||
() => GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"set terrain atlas anisotropy",
|
||||
() =>
|
||||
{
|
||||
// GL_TEXTURE_MAX_ANISOTROPY = 0x84FE
|
||||
_gl.TexParameter(
|
||||
TextureTarget.Texture2DArray,
|
||||
(TextureParameterName)0x84FE,
|
||||
(float)level);
|
||||
}));
|
||||
}
|
||||
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, GlTexture);
|
||||
// GL_TEXTURE_MAX_ANISOTROPY = 0x84FE
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, (float)level);
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
|
||||
// Re-generate bindless handles if they were live before.
|
||||
if (wasResident)
|
||||
{
|
||||
// GetBindlessHandles regenerates and makes resident.
|
||||
_ = GetBindlessHandles();
|
||||
}
|
||||
if (_bindlessMutation is not null)
|
||||
_bindlessMutation.Execute(Mutate);
|
||||
else
|
||||
Mutate();
|
||||
|
||||
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Phase 1: release bindless residency BEFORE deleting textures.
|
||||
// ARB_bindless_texture requires this ordering; interleaving is UB.
|
||||
if (_handlesGenerated && _bindless is not null)
|
||||
{
|
||||
_bindless.MakeNonResident(_terrainHandle);
|
||||
_bindless.MakeNonResident(_alphaHandle);
|
||||
_handlesGenerated = false;
|
||||
}
|
||||
|
||||
// Phase 2: delete the underlying GL textures.
|
||||
_gl.DeleteTexture(GlTexture);
|
||||
_gl.DeleteTexture(GlAlphaTexture);
|
||||
_shutdown ??= new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage(
|
||||
"terrain atlas bindless residency",
|
||||
[
|
||||
new ResourceShutdownOperation(
|
||||
"release terrain and alpha handles",
|
||||
() => _bindlessHandles?.Release()),
|
||||
]),
|
||||
new ResourceShutdownStage(
|
||||
"terrain atlas textures",
|
||||
[
|
||||
new ResourceShutdownOperation(
|
||||
"delete terrain texture",
|
||||
() => GlResourceCommand.DeleteTexture(
|
||||
_gl,
|
||||
GlTexture,
|
||||
$"delete terrain atlas texture {GlTexture}")),
|
||||
new ResourceShutdownOperation(
|
||||
"delete alpha texture",
|
||||
() => GlResourceCommand.DeleteTexture(
|
||||
_gl,
|
||||
GlAlphaTexture,
|
||||
$"delete terrain alpha texture {GlAlphaTexture}")),
|
||||
]));
|
||||
_shutdown.CompleteOrThrow();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public sealed unsafe class TextRenderer : IDisposable
|
|||
private readonly GL _gl;
|
||||
private readonly ITextRenderGlStateApi _glState;
|
||||
private readonly Shader _shader;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
private uint _vao;
|
||||
private uint _vbo;
|
||||
private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
|
||||
|
|
@ -39,7 +40,7 @@ public sealed unsafe class TextRenderer : IDisposable
|
|||
public int UsedBytes;
|
||||
}
|
||||
|
||||
private readonly FrameBufferSet[] _frameBuffers = new FrameBufferSet[3];
|
||||
private readonly FrameBufferSet[] _frameBuffers;
|
||||
private FrameBufferSet? _activeFrameBuffer;
|
||||
|
||||
internal long DynamicBufferCapacityBytes =>
|
||||
|
|
@ -80,26 +81,70 @@ public sealed unsafe class TextRenderer : IDisposable
|
|||
{
|
||||
_gl = gl;
|
||||
_glState = new SilkTextRenderGlStateApi(gl);
|
||||
_shader = new Shader(gl,
|
||||
Path.Combine(shaderDir, "ui_text.vert"),
|
||||
Path.Combine(shaderDir, "ui_text.frag"));
|
||||
var resources = new ResourceCleanupGroup();
|
||||
Shader? shader = null;
|
||||
var frameBuffers = new FrameBufferSet[3];
|
||||
uint whiteTexture = 0;
|
||||
try
|
||||
{
|
||||
shader = new Shader(gl,
|
||||
Path.Combine(shaderDir, "ui_text.vert"),
|
||||
Path.Combine(shaderDir, "ui_text.frag"));
|
||||
resources.Add("text shader", shader.Dispose);
|
||||
|
||||
for (int i = 0; i < _frameBuffers.Length; i++)
|
||||
_frameBuffers[i] = CreateFrameBufferSet();
|
||||
for (int i = 0; i < frameBuffers.Length; i++)
|
||||
frameBuffers[i] = CreateFrameBufferSet(resources);
|
||||
|
||||
// 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
|
||||
// bucket (the shader multiplies texel×color → white×color = color). Lets a panel
|
||||
// background draw UNDER its text in painter order, which DrawRect's separate
|
||||
// bucket cannot (it always composites after all sprites).
|
||||
_whiteTex = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2D, _whiteTex);
|
||||
Span<byte> whitePixel = stackalloc byte[] { 255, 255, 255, 255 };
|
||||
fixed (byte* wp = whitePixel)
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, wp);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
// 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
|
||||
// bucket (the shader multiplies texel×color → white×color = color). Lets a panel
|
||||
// background draw UNDER its text in painter order, which DrawRect's separate
|
||||
// bucket cannot (it always composites after all sprites).
|
||||
whiteTexture = GlResourceCommand.CreateTexture(
|
||||
_gl,
|
||||
"TextRenderer white texture");
|
||||
uint ownedWhiteTexture = whiteTexture;
|
||||
resources.Add(
|
||||
"white texture",
|
||||
() => GlResourceCommand.DeleteTexture(
|
||||
_gl,
|
||||
ownedWhiteTexture,
|
||||
$"delete TextRenderer white texture {ownedWhiteTexture}"));
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"initialize TextRenderer white texture",
|
||||
() =>
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2D, whiteTexture);
|
||||
Span<byte> whitePixel = stackalloc byte[] { 255, 255, 255, 255 };
|
||||
fixed (byte* wp = whitePixel)
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0,
|
||||
PixelFormat.Rgba, PixelType.UnsignedByte, wp);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
});
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
resources.RetryCleanup();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
"TextRenderer construction failed and its published GL resources did not cleanly roll back.",
|
||||
resources,
|
||||
[constructionFailure, cleanupFailure]);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
_resources = resources;
|
||||
_shader = shader;
|
||||
_frameBuffers = frameBuffers;
|
||||
_whiteTex = whiteTexture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -120,43 +165,54 @@ public sealed unsafe class TextRenderer : IDisposable
|
|||
_vboCapacityBytes = set.CapacityBytes;
|
||||
}
|
||||
|
||||
private FrameBufferSet CreateFrameBufferSet()
|
||||
private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)
|
||||
{
|
||||
uint vao = 0;
|
||||
uint vbo = 0;
|
||||
try
|
||||
{
|
||||
vao = TrackedGlResource.CreateVertexArray(
|
||||
uint vao = TrackedGlResource.CreateVertexArray(
|
||||
_gl,
|
||||
"TextRenderer frame VAO creation");
|
||||
RetryableGpuResourceRelease vaoRelease =
|
||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
||||
_gl,
|
||||
"TextRenderer frame VAO creation");
|
||||
vbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"TextRenderer frame VBO creation");
|
||||
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
|
||||
vao,
|
||||
"TextRenderer frame VAO disposal");
|
||||
resources.Add("frame VAO", vaoRelease.Run);
|
||||
var set = new FrameBufferSet { Vao = vao };
|
||||
|
||||
_gl.BindVertexArray(set.Vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
|
||||
uint stride = FloatsPerVertex * sizeof(float);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float)));
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
return set;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (vbo != 0)
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl, vbo, 0, "TextRenderer frame VBO rollback");
|
||||
if (vao != 0)
|
||||
TrackedGlResource.DeleteVertexArray(
|
||||
_gl, vao, "TextRenderer frame VAO rollback");
|
||||
throw;
|
||||
}
|
||||
uint vbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"TextRenderer frame VBO creation");
|
||||
set.Vbo = vbo;
|
||||
RetryableGpuResourceRelease? vboRelease = null;
|
||||
resources.Add(
|
||||
"frame VBO",
|
||||
() =>
|
||||
{
|
||||
vboRelease ??= TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
vbo,
|
||||
set.CapacityBytes,
|
||||
"TextRenderer frame VBO disposal");
|
||||
vboRelease.Run();
|
||||
});
|
||||
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"initialize TextRenderer frame VAO and VBO",
|
||||
() =>
|
||||
{
|
||||
_gl.BindVertexArray(set.Vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
|
||||
uint stride = FloatsPerVertex * sizeof(float);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float)));
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
||||
|
|
@ -471,19 +527,6 @@ public sealed unsafe class TextRenderer : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteTexture(_whiteTex);
|
||||
foreach (FrameBufferSet set in _frameBuffers)
|
||||
{
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
set.Vbo,
|
||||
set.CapacityBytes,
|
||||
"TextRenderer frame VBO disposal");
|
||||
TrackedGlResource.DeleteVertexArray(
|
||||
_gl,
|
||||
set.Vao,
|
||||
"TextRenderer frame VAO disposal");
|
||||
}
|
||||
_shader.Dispose();
|
||||
_resources.RetryCleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/AcDream.App/Rendering/TrackedTextureConstruction.cs
Normal file
31
src/AcDream.App/Rendering/TrackedTextureConstruction.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal static class TrackedTextureConstruction
|
||||
{
|
||||
public static uint Create(
|
||||
GlTextureConstructionTransaction transaction,
|
||||
Action<uint> initialize)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(transaction);
|
||||
ArgumentNullException.ThrowIfNull(initialize);
|
||||
uint texture = transaction.Allocate();
|
||||
initialize(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static uint Create(
|
||||
GlTextureConstructionTransaction transaction,
|
||||
Silk.NET.OpenGL.GL gl,
|
||||
string context,
|
||||
Action<uint> initialize)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(transaction);
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(context);
|
||||
ArgumentNullException.ThrowIfNull(initialize);
|
||||
|
||||
uint texture = transaction.Allocate();
|
||||
GlResourceCommand.Execute(gl, context, () => initialize(texture));
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.OpenGL.Extensions.ARB;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -10,10 +11,12 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </summary>
|
||||
public sealed class BindlessSupport
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly ArbBindlessTexture _ext;
|
||||
|
||||
private BindlessSupport(ArbBindlessTexture extension)
|
||||
private BindlessSupport(GL gl, ArbBindlessTexture extension)
|
||||
{
|
||||
_gl = gl;
|
||||
_ext = extension;
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +24,7 @@ public sealed class BindlessSupport
|
|||
{
|
||||
if (gl.TryGetExtension<ArbBindlessTexture>(out var ext))
|
||||
{
|
||||
support = new BindlessSupport(ext);
|
||||
support = new BindlessSupport(gl, ext);
|
||||
return true;
|
||||
}
|
||||
support = null;
|
||||
|
|
@ -32,17 +35,42 @@ public sealed class BindlessSupport
|
|||
/// Idempotent: handle is the same for a given texture name.</summary>
|
||||
public ulong GetResidentHandle(uint textureName)
|
||||
{
|
||||
ulong h = _ext.GetTextureHandle(textureName);
|
||||
if (!_ext.IsTextureHandleResident(h))
|
||||
_ext.MakeTextureHandleResident(h);
|
||||
ulong h = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"get bindless handle for texture {textureName}",
|
||||
() => _ext.GetTextureHandle(textureName));
|
||||
if (h == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL returned no bindless handle for texture {textureName}.");
|
||||
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {h} residency",
|
||||
() => _ext.IsTextureHandleResident(h));
|
||||
if (!resident)
|
||||
{
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {h} resident",
|
||||
() => _ext.MakeTextureHandleResident(h));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>Release residency for a handle. Call before deleting the underlying texture.</summary>
|
||||
public void MakeNonResident(ulong handle)
|
||||
{
|
||||
if (_ext.IsTextureHandleResident(handle))
|
||||
_ext.MakeTextureHandleNonResident(handle);
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {handle} residency before release",
|
||||
() => _ext.IsTextureHandleResident(handle));
|
||||
if (!resident)
|
||||
return;
|
||||
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {handle} non-resident",
|
||||
() => _ext.MakeTextureHandleNonResident(handle));
|
||||
}
|
||||
|
||||
// Phase N.5b note: a `SetSamplerHandleUniform` wrapper was added in T6
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using AcDream.App.Rendering;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -96,41 +97,68 @@ internal static unsafe class TrackedGlResource
|
|||
|
||||
public static uint CreateBuffer(GL gl, string context)
|
||||
{
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
uint name = gl.GenBuffer();
|
||||
try
|
||||
{
|
||||
if (name == 0)
|
||||
throw new InvalidOperationException($"OpenGL returned buffer name zero. Context: {context}");
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
return name;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (name != 0)
|
||||
gl.DeleteBuffer(name);
|
||||
throw;
|
||||
}
|
||||
return CreateTrackedName(
|
||||
gl,
|
||||
"buffer",
|
||||
context,
|
||||
gl.GenBuffer,
|
||||
name => GlResourceCommand.DeleteBuffer(
|
||||
gl,
|
||||
name,
|
||||
$"rollback buffer {name} after failed {context}"),
|
||||
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
||||
public static uint CreateVertexArray(GL gl, string context)
|
||||
{
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
uint name = gl.GenVertexArray();
|
||||
return CreateTrackedName(
|
||||
gl,
|
||||
"vertex-array",
|
||||
context,
|
||||
gl.GenVertexArray,
|
||||
name => GlResourceCommand.DeleteVertexArray(
|
||||
gl,
|
||||
name,
|
||||
$"rollback vertex array {name} after failed {context}"),
|
||||
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO));
|
||||
}
|
||||
|
||||
private static uint CreateTrackedName(
|
||||
GL gl,
|
||||
string resourceName,
|
||||
string context,
|
||||
Func<uint> create,
|
||||
Action<uint> rollback,
|
||||
Action publishAccounting)
|
||||
{
|
||||
uint name = GlResourceCommand.CreateName(
|
||||
gl,
|
||||
$"{resourceName} for {context}",
|
||||
create,
|
||||
rollback);
|
||||
var cleanup = new ResourceCleanupGroup();
|
||||
cleanup.Add($"{resourceName} name {name}", () => rollback(name));
|
||||
try
|
||||
{
|
||||
if (name == 0)
|
||||
throw new InvalidOperationException($"OpenGL returned vertex-array name zero. Context: {context}");
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
|
||||
publishAccounting();
|
||||
return name;
|
||||
}
|
||||
catch
|
||||
catch (Exception publicationFailure)
|
||||
{
|
||||
if (name != 0)
|
||||
gl.DeleteVertexArray(name);
|
||||
throw;
|
||||
try
|
||||
{
|
||||
cleanup.RetryCleanup();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
$"Publishing {resourceName} accounting failed and GL name {name} could not be released.",
|
||||
cleanup,
|
||||
[publicationFailure, cleanupFailure]);
|
||||
}
|
||||
|
||||
ExceptionDispatchInfo.Capture(publicationFailure).Throw();
|
||||
throw new InvalidOperationException("Unreachable resource-publication path.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private ResourceShutdownTransaction? _shutdown;
|
||||
private bool _disposed;
|
||||
|
||||
internal bool IsDisposalComplete => _disposed;
|
||||
|
||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
_bindings = bindings;
|
||||
|
|
@ -211,6 +213,15 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
bindings.Host.HideWindow);
|
||||
}
|
||||
|
||||
internal static RetailUiRuntime CreateUninitialized(
|
||||
RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bindings);
|
||||
return new RetailUiRuntime(bindings);
|
||||
}
|
||||
|
||||
internal void InitializeForLease() => Initialize();
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
RetailUiRuntimeBindings bindings = _bindings;
|
||||
|
|
@ -298,7 +309,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bindings);
|
||||
var runtime = new RetailUiRuntime(bindings);
|
||||
var runtime = CreateUninitialized(bindings);
|
||||
try
|
||||
{
|
||||
runtime.Initialize();
|
||||
|
|
|
|||
263
src/AcDream.App/UI/RetailUiRuntimeLease.cs
Normal file
263
src/AcDream.App/UI/RetailUiRuntimeLease.cs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Retains one UiHost/runtime ownership chain across partial construction,
|
||||
/// initialization cleanup, lifetime retry, and explicit terminal abandonment.
|
||||
/// </summary>
|
||||
internal sealed class RetailUiRuntimeLease : IDisposable
|
||||
{
|
||||
private object? _host;
|
||||
private Action? _quiesceInput;
|
||||
private Action? _deactivateInput;
|
||||
private Action? _disposeHost;
|
||||
private Func<bool>? _hostDisposalComplete;
|
||||
|
||||
private object? _runtime;
|
||||
private Action? _disposeRuntime;
|
||||
private Func<bool>? _runtimeDisposalComplete;
|
||||
|
||||
private bool _disposalFailed;
|
||||
private bool _inputQuiesced;
|
||||
private bool _inputDeactivated;
|
||||
private bool _operationActive;
|
||||
private bool _disposed;
|
||||
private bool _abandoned;
|
||||
|
||||
public bool IsDisposalComplete => _disposed;
|
||||
|
||||
public bool IsAbandoned => _abandoned;
|
||||
|
||||
internal bool RetainsResources => _host is not null || _runtime is not null;
|
||||
|
||||
public UiHost AcquireHost(Func<UiHost> factory) => AcquireHostCore(
|
||||
factory,
|
||||
static host => host.QuiesceInput(),
|
||||
static host => host.DeactivateInput(),
|
||||
static host => host.Dispose(),
|
||||
static host => host.IsDisposalComplete);
|
||||
|
||||
internal T AcquireHostCore<T>(
|
||||
Func<T> factory,
|
||||
Action<T> quiesceInput,
|
||||
Action<T> deactivateInput,
|
||||
Action<T> dispose,
|
||||
Func<T, bool> disposalComplete)
|
||||
where T : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
ArgumentNullException.ThrowIfNull(quiesceInput);
|
||||
ArgumentNullException.ThrowIfNull(deactivateInput);
|
||||
ArgumentNullException.ThrowIfNull(dispose);
|
||||
ArgumentNullException.ThrowIfNull(disposalComplete);
|
||||
ThrowIfUnavailable();
|
||||
if (_operationActive || _host is not null)
|
||||
throw new InvalidOperationException("A retained UI host is already owned.");
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
T host = factory()
|
||||
?? throw new InvalidOperationException("The retained UI host factory returned null.");
|
||||
_host = host;
|
||||
_quiesceInput = () => quiesceInput(host);
|
||||
_deactivateInput = () => deactivateInput(host);
|
||||
_disposeHost = () => dispose(host);
|
||||
_hostDisposalComplete = () => disposalComplete(host);
|
||||
return host;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public RetailUiRuntime Mount(Func<RetailUiRuntime> factory) => MountCore(
|
||||
factory,
|
||||
static runtime => runtime.InitializeForLease(),
|
||||
static runtime => runtime.Dispose(),
|
||||
static runtime => runtime.IsDisposalComplete);
|
||||
|
||||
internal T MountCore<T>(
|
||||
Func<T> factory,
|
||||
Action<T> initialize,
|
||||
Action<T> dispose,
|
||||
Func<T, bool> disposalComplete)
|
||||
where T : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
ArgumentNullException.ThrowIfNull(initialize);
|
||||
ArgumentNullException.ThrowIfNull(dispose);
|
||||
ArgumentNullException.ThrowIfNull(disposalComplete);
|
||||
ThrowIfUnavailable();
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The retained UI lease is changing state.");
|
||||
if (_host is null)
|
||||
throw new InvalidOperationException("The retained UI host must be acquired first.");
|
||||
if (_runtime is not null)
|
||||
throw new InvalidOperationException("A retained UI runtime is already owned.");
|
||||
|
||||
// Construction may throw while the lease still owns the host directly.
|
||||
_operationActive = true;
|
||||
T runtime;
|
||||
try
|
||||
{
|
||||
runtime = factory()
|
||||
?? throw new InvalidOperationException("The retained UI runtime factory returned null.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
|
||||
// Publish the partial runtime before any Initialize side effect.
|
||||
_runtime = runtime;
|
||||
_disposeRuntime = () => dispose(runtime);
|
||||
_runtimeDisposalComplete = () => disposalComplete(runtime);
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
initialize(runtime);
|
||||
_operationActive = false;
|
||||
return runtime;
|
||||
}
|
||||
catch (Exception initializationFailure)
|
||||
{
|
||||
_operationActive = false;
|
||||
try
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Retail UI initialization failed and its published partial runtime did not cleanly retire.",
|
||||
initializationFailure,
|
||||
cleanupFailure);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void QuiesceInput()
|
||||
{
|
||||
if (_disposed || _abandoned || _inputQuiesced)
|
||||
return;
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The retained UI lease is changing state.");
|
||||
|
||||
_operationActive = true;
|
||||
_inputQuiesced = true;
|
||||
try
|
||||
{
|
||||
_quiesceInput?.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_inputQuiesced = false;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void DeactivateInput()
|
||||
{
|
||||
if (_disposed || _abandoned || _inputDeactivated)
|
||||
return;
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The retained UI lease is changing state.");
|
||||
|
||||
_operationActive = true;
|
||||
_inputDeactivated = true;
|
||||
try
|
||||
{
|
||||
_deactivateInput?.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_inputDeactivated = false;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed || _abandoned)
|
||||
return;
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The retained UI lease is changing state.");
|
||||
|
||||
_operationActive = true;
|
||||
try
|
||||
{
|
||||
if (_runtime is not null)
|
||||
{
|
||||
_disposeRuntime!();
|
||||
if (_runtimeDisposalComplete?.Invoke() != true)
|
||||
throw new InvalidOperationException(
|
||||
"The retained UI runtime returned without completing disposal.");
|
||||
if (_hostDisposalComplete?.Invoke() != true)
|
||||
throw new InvalidOperationException(
|
||||
"The retained UI runtime completed without retiring its host.");
|
||||
}
|
||||
else if (_host is not null)
|
||||
{
|
||||
_disposeHost!();
|
||||
if (_hostDisposalComplete?.Invoke() != true)
|
||||
throw new InvalidOperationException(
|
||||
"The retained UI host returned without completing disposal.");
|
||||
}
|
||||
|
||||
_runtime = null;
|
||||
_disposeRuntime = null;
|
||||
_runtimeDisposalComplete = null;
|
||||
_host = null;
|
||||
_quiesceInput = null;
|
||||
_deactivateInput = null;
|
||||
_disposeHost = null;
|
||||
_hostDisposalComplete = null;
|
||||
_disposalFailed = false;
|
||||
_disposed = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_disposalFailed = true;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void AbandonAfterTerminalFailure()
|
||||
{
|
||||
if (_disposed || _abandoned)
|
||||
return;
|
||||
if (_operationActive)
|
||||
throw new InvalidOperationException("The retained UI lease is changing state.");
|
||||
if (!_disposalFailed)
|
||||
throw new InvalidOperationException(
|
||||
"Retained UI resources may be abandoned only after disposal failed.");
|
||||
|
||||
// Keep the exact references rooted. Checkpoint J owns the final native
|
||||
// fallback/reporting policy; abandonment must not turn this into an
|
||||
// unowned local resource.
|
||||
_abandoned = true;
|
||||
}
|
||||
|
||||
private void ThrowIfUnavailable()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_abandoned)
|
||||
throw new InvalidOperationException("The retained UI lease was abandoned.");
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,8 @@ public sealed class UiHost : System.IDisposable
|
|||
private bool _disposeRequested;
|
||||
private bool _disposed;
|
||||
|
||||
internal bool IsDisposalComplete => _disposed;
|
||||
|
||||
public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null)
|
||||
: this(gl, shaderDir, defaultFont, new HostQuiescenceGate())
|
||||
{
|
||||
|
|
@ -67,9 +69,9 @@ public sealed class UiHost : System.IDisposable
|
|||
BitmapFont? defaultFont,
|
||||
HostQuiescenceGate quiescence)
|
||||
{
|
||||
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
|
||||
TextRenderer = new TextRenderer(gl, shaderDir);
|
||||
DefaultFont = defaultFont;
|
||||
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
|
||||
}
|
||||
|
||||
// ── Per-frame ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ internal interface ICameraFramePhase
|
|||
/// <see cref="IRetailLiveFramePhase"/>. This host order is the accepted TS-53
|
||||
/// adaptation and is not claimed to be the exact retail Client::UseTime order.
|
||||
/// </remarks>
|
||||
internal sealed class UpdateFrameOrchestrator
|
||||
internal sealed class UpdateFrameOrchestrator : AcDream.App.Rendering.IGameUpdateFrameRoot
|
||||
{
|
||||
private readonly IUpdateFrameTeardownPhase _teardown;
|
||||
private readonly IUpdateFrameFailureSink _failureSink;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue