fix(rendering): bound portal resource lifetime

Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-18 21:35:16 +02:00
parent 3971997689
commit 749e8ceeb1
225 changed files with 29107 additions and 3914 deletions

View file

@ -50,6 +50,8 @@ public sealed class UiHost : System.IDisposable
private long _startTicks = System.Environment.TickCount64;
private readonly List<System.Action> _inputUnsubscribers = new();
private ResourceShutdownTransaction? _shutdown;
private bool _disposeRequested;
private bool _disposed;
public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null)
@ -81,7 +83,7 @@ public sealed class UiHost : System.IDisposable
public void WireMouse(IMouse mouse)
{
System.ObjectDisposedException.ThrowIf(_disposed, this);
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
System.ArgumentNullException.ThrowIfNull(mouse);
void OnMouseDown(IMouse sender, MouseButton button) =>
@ -107,7 +109,7 @@ public sealed class UiHost : System.IDisposable
public void WireKeyboard(IKeyboard kb)
{
System.ObjectDisposedException.ThrowIf(_disposed, this);
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
System.ArgumentNullException.ThrowIfNull(kb);
Keyboard = kb; // last wired keyboard wins (one-keyboard desktop)
void OnKeyDown(IKeyboard sender, Key key, int scanCode) => Root.OnKeyDown((int)key);
@ -162,13 +164,55 @@ public sealed class UiHost : System.IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
for (int i = _inputUnsubscribers.Count - 1; i >= 0; i--)
_inputUnsubscribers[i]();
_inputUnsubscribers.Clear();
Keyboard = null;
WindowManager.Dispose();
TextRenderer.Dispose();
if (_disposed)
return;
_disposeRequested = true;
_shutdown ??= CreateShutdownTransaction(
_inputUnsubscribers.AsEnumerable().Reverse().ToArray(),
() =>
{
_inputUnsubscribers.Clear();
Keyboard = null;
},
WindowManager.Dispose,
TextRenderer.Dispose);
_shutdown.CompleteOrThrow();
_disposed = _shutdown.IsComplete;
}
internal static ResourceShutdownTransaction CreateShutdownTransaction(
IReadOnlyList<Action> inputUnsubscribers,
Action releaseInputState,
Action disposeWindowManager,
Action disposeTextRenderer)
{
ArgumentNullException.ThrowIfNull(inputUnsubscribers);
ArgumentNullException.ThrowIfNull(releaseInputState);
ArgumentNullException.ThrowIfNull(disposeWindowManager);
ArgumentNullException.ThrowIfNull(disposeTextRenderer);
return new ResourceShutdownTransaction(
new ResourceShutdownStage(
"retained UI input subscriptions",
inputUnsubscribers
.Select((unsubscribe, index) => new ResourceShutdownOperation(
$"input subscription {index}",
unsubscribe ?? throw new ArgumentException(
"Input unsubscriber entries cannot be null.",
nameof(inputUnsubscribers))))
.ToArray()),
new ResourceShutdownStage("retained UI input state",
[
new("input state", releaseInputState),
]),
new ResourceShutdownStage("retained UI windows",
[
new("window manager", disposeWindowManager),
]),
new ResourceShutdownStage("retained UI renderer",
[
new("text renderer", disposeTextRenderer),
]));
}
}