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:
Erik 2026-07-22 14:42:14 +02:00
parent fec0d94148
commit c87b15303d
41 changed files with 3768 additions and 355 deletions

View file

@ -0,0 +1,352 @@
using AcDream.App.UI;
namespace AcDream.App.Tests.UI;
public sealed class RetailUiRuntimeLeaseTests
{
[Fact]
public void ConstructorFailureLeavesHostOwnedUntilLifetimeDisposal()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
Assert.Throws<InvalidOperationException>(() =>
lease.MountCore<FakeRuntime>(
() => throw new InvalidOperationException("constructor failed"),
static runtime => runtime.Initialize(),
static runtime => runtime.Dispose(),
static runtime => runtime.IsDisposalComplete));
Assert.True(lease.RetainsResources);
Assert.Equal(0, host.DisposeCalls);
lease.Dispose();
lease.Dispose();
Assert.True(lease.IsDisposalComplete);
Assert.False(lease.RetainsResources);
Assert.Equal(1, host.DisposeCalls);
}
[Fact]
public void InitializeFailureCleansRuntimeAndHostExactlyOnce()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
var runtime = new FakeRuntime(host) { InitializeFailure = true };
InvalidOperationException failure = Assert.Throws<InvalidOperationException>(() =>
Mount(lease, runtime));
Assert.Equal("initialize failed", failure.Message);
Assert.True(lease.IsDisposalComplete);
Assert.False(lease.RetainsResources);
Assert.Equal(1, runtime.DisposeCalls);
Assert.Equal(1, host.DisposeCalls);
}
[Fact]
public void TransientInitializeCleanupFailureRetainsOwnershipForLifetimeRetry()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
var runtime = new FakeRuntime(host)
{
InitializeFailure = true,
RemainingDisposeFailures = 1,
};
AggregateException failure = Assert.Throws<AggregateException>(() =>
Mount(lease, runtime));
Assert.Equal(2, failure.InnerExceptions.Count);
Assert.True(lease.RetainsResources);
Assert.False(lease.IsDisposalComplete);
Assert.Equal(1, runtime.DisposeCalls);
Assert.Equal(0, host.DisposeCalls);
lease.Dispose();
Assert.True(lease.IsDisposalComplete);
Assert.False(lease.RetainsResources);
Assert.Equal(2, runtime.DisposeCalls);
Assert.Equal(1, host.DisposeCalls);
}
[Fact]
public void PersistentCleanupFailureCanBeExplicitlyAbandonedAndBecomesInert()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
var runtime = new FakeRuntime(host)
{
InitializeFailure = true,
RemainingDisposeFailures = int.MaxValue,
};
Assert.Throws<AggregateException>(() => Mount(lease, runtime));
Assert.Throws<InvalidOperationException>(lease.Dispose);
Assert.Throws<InvalidOperationException>(() =>
new RetailUiRuntimeLease().AbandonAfterTerminalFailure());
lease.AbandonAfterTerminalFailure();
lease.Dispose();
lease.QuiesceInput();
lease.DeactivateInput();
Assert.True(lease.IsAbandoned);
Assert.False(lease.IsDisposalComplete);
Assert.True(lease.RetainsResources);
Assert.Equal(2, runtime.DisposeCalls);
Assert.Equal(0, host.DisposeCalls);
}
[Fact]
public void QuiesceAndDeactivateAreSeparateIdempotentOperations()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
lease.QuiesceInput();
lease.QuiesceInput();
lease.DeactivateInput();
lease.DeactivateInput();
Assert.Equal(1, host.QuiesceCalls);
Assert.Equal(1, host.DeactivateCalls);
Assert.Equal(0, host.DisposeCalls);
lease.Dispose();
Assert.Equal(1, host.DisposeCalls);
}
[Fact]
public void HostAndRuntimeMayOnlyBePublishedOnce()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
var runtime = new FakeRuntime(host);
Assert.Throws<InvalidOperationException>(() => AcquireHost(lease));
Assert.Same(runtime, Mount(lease, runtime));
Assert.Throws<InvalidOperationException>(() =>
Mount(lease, new FakeRuntime(host)));
lease.Dispose();
}
[Fact]
public void ReentrantHostAndRuntimeFactoriesCannotOverwriteOuterOwnership()
{
var lease = new RetailUiRuntimeLease();
var host = new FakeHost();
int nestedHosts = 0;
FakeHost acquired = lease.AcquireHostCore(
() =>
{
Assert.Throws<InvalidOperationException>(() =>
lease.AcquireHostCore(
() =>
{
nestedHosts++;
return new FakeHost();
},
static value => value.QuiesceInput(),
static value => value.DeactivateInput(),
static value => value.Dispose(),
static value => value.IsDisposalComplete));
return host;
},
static value => value.QuiesceInput(),
static value => value.DeactivateInput(),
static value => value.Dispose(),
static value => value.IsDisposalComplete);
var runtime = new FakeRuntime(host);
int nestedRuntimes = 0;
FakeRuntime mounted = lease.MountCore(
() =>
{
Assert.Throws<InvalidOperationException>(() =>
lease.MountCore(
() =>
{
nestedRuntimes++;
return new FakeRuntime(host);
},
static value => value.Initialize(),
static value => value.Dispose(),
static value => value.IsDisposalComplete));
return runtime;
},
static value => value.Initialize(),
static value => value.Dispose(),
static value => value.IsDisposalComplete);
Assert.Same(host, acquired);
Assert.Same(runtime, mounted);
Assert.Equal(0, nestedHosts);
Assert.Equal(0, nestedRuntimes);
lease.Dispose();
}
[Fact]
public void ReentrantDisposeIsRejectedWithoutDoubleDisposingRuntimeOrHost()
{
var lease = new RetailUiRuntimeLease();
var host = AcquireHost(lease);
var runtime = new FakeRuntime(host)
{
OnDispose = () =>
Assert.Throws<InvalidOperationException>(lease.Dispose),
};
Mount(lease, runtime);
lease.Dispose();
Assert.Equal(1, runtime.DisposeCalls);
Assert.Equal(1, host.DisposeCalls);
Assert.True(lease.IsDisposalComplete);
}
[Fact]
public void HostFactoryCannotPrematurelyCompleteInputCutoffOperations()
{
var lease = new RetailUiRuntimeLease();
var host = new FakeHost();
lease.AcquireHostCore(
() =>
{
Assert.Throws<InvalidOperationException>(lease.QuiesceInput);
Assert.Throws<InvalidOperationException>(lease.DeactivateInput);
return host;
},
static value => value.QuiesceInput(),
static value => value.DeactivateInput(),
static value => value.Dispose(),
static value => value.IsDisposalComplete);
lease.QuiesceInput();
lease.DeactivateInput();
Assert.Equal(1, host.QuiesceCalls);
Assert.Equal(1, host.DeactivateCalls);
lease.Dispose();
}
[Fact]
public void InputCutoffRejectsReentrantMutationAndRetriesFailedCallback()
{
var lease = new RetailUiRuntimeLease();
var host = new FakeHost();
int quiesceAttempts = 0;
int deactivateAttempts = 0;
lease.AcquireHostCore(
() => host,
_ =>
{
quiesceAttempts++;
Assert.Throws<InvalidOperationException>(() =>
lease.MountCore(
() => new FakeRuntime(host),
static value => value.Initialize(),
static value => value.Dispose(),
static value => value.IsDisposalComplete));
if (quiesceAttempts == 1)
throw new InvalidOperationException("quiesce failed");
host.QuiesceInput();
},
_ =>
{
deactivateAttempts++;
Assert.Throws<InvalidOperationException>(lease.Dispose);
if (deactivateAttempts == 1)
throw new InvalidOperationException("deactivate failed");
host.DeactivateInput();
},
static value => value.Dispose(),
static value => value.IsDisposalComplete);
Assert.Throws<InvalidOperationException>(lease.QuiesceInput);
lease.QuiesceInput();
Assert.Throws<InvalidOperationException>(lease.DeactivateInput);
lease.DeactivateInput();
Assert.Equal(2, quiesceAttempts);
Assert.Equal(2, deactivateAttempts);
Assert.Equal(1, host.QuiesceCalls);
Assert.Equal(1, host.DeactivateCalls);
lease.Dispose();
}
private static FakeHost AcquireHost(RetailUiRuntimeLease lease)
{
var host = new FakeHost();
return lease.AcquireHostCore(
() => host,
static value => value.QuiesceInput(),
static value => value.DeactivateInput(),
static value => value.Dispose(),
static value => value.IsDisposalComplete);
}
private static FakeRuntime Mount(
RetailUiRuntimeLease lease,
FakeRuntime runtime) => lease.MountCore(
() => runtime,
static value => value.Initialize(),
static value => value.Dispose(),
static value => value.IsDisposalComplete);
private sealed class FakeHost : IDisposable
{
public int QuiesceCalls { get; private set; }
public int DeactivateCalls { get; private set; }
public int DisposeCalls { get; private set; }
public bool IsDisposalComplete { get; private set; }
public void QuiesceInput() => QuiesceCalls++;
public void DeactivateInput() => DeactivateCalls++;
public void Dispose()
{
if (IsDisposalComplete)
return;
DisposeCalls++;
IsDisposalComplete = true;
}
}
private sealed class FakeRuntime(FakeHost host) : IDisposable
{
public bool InitializeFailure { get; init; }
public int RemainingDisposeFailures { get; set; }
public int DisposeCalls { get; private set; }
public bool IsDisposalComplete { get; private set; }
public Action? OnDispose { get; init; }
public void Initialize()
{
if (InitializeFailure)
throw new InvalidOperationException("initialize failed");
}
public void Dispose()
{
if (IsDisposalComplete)
return;
DisposeCalls++;
OnDispose?.Invoke();
if (RemainingDisposeFailures-- > 0)
throw new InvalidOperationException("dispose failed");
host.Dispose();
IsDisposalComplete = true;
}
}
}