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
|
|
@ -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 ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue