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:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
135
src/AcDream.App/Rendering/Wb/RetryableResourceReleaseLedger.cs
Normal file
135
src/AcDream.App/Rendering/Wb/RetryableResourceReleaseLedger.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Per-resource progress for a teardown which must attempt every independent
|
||||
/// release even when an earlier release fails. Successful operations are never
|
||||
/// replayed. A backend which throws after changing ownership reports that fact
|
||||
/// with <see cref="MeshReferenceMutationException.MutationCommitted"/> so the
|
||||
/// ledger can preserve the physical outcome while still surfacing the error.
|
||||
/// </summary>
|
||||
internal sealed class RetryableResourceReleaseLedger
|
||||
{
|
||||
private readonly Entry[] _entries;
|
||||
private int _remaining;
|
||||
private bool _advancing;
|
||||
|
||||
public RetryableResourceReleaseLedger(IEnumerable<(string Name, Action Release)> entries)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entries);
|
||||
_entries = entries
|
||||
.Select(entry => new Entry(entry.Name, entry.Release))
|
||||
.ToArray();
|
||||
_remaining = _entries.Length;
|
||||
}
|
||||
|
||||
public bool IsComplete => _remaining == 0;
|
||||
public int RemainingCount => _remaining;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts every unfinished operation once. Ordinary exceptions retain
|
||||
/// the operation for a later attempt; committed-outcome exceptions mark it
|
||||
/// complete. Either kind remains in the returned diagnostics.
|
||||
/// </summary>
|
||||
public ResourceReleaseAttempt Advance()
|
||||
{
|
||||
// Release callbacks may synchronously drain their owner. The active
|
||||
// top-level pass already owns every unfinished entry; a nested pass
|
||||
// must not give a failed later entry a second attempt in this frame.
|
||||
if (_advancing)
|
||||
return new ResourceReleaseAttempt(0, 0, []);
|
||||
|
||||
_advancing = true;
|
||||
List<ResourceReleaseFailure>? failures = null;
|
||||
int attempted = 0;
|
||||
int completed = 0;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < _entries.Length; i++)
|
||||
{
|
||||
Entry entry = _entries[i];
|
||||
if (entry.Completed || entry.Running)
|
||||
continue;
|
||||
|
||||
attempted++;
|
||||
entry.Running = true;
|
||||
try
|
||||
{
|
||||
entry.Release();
|
||||
entry.Completed = true;
|
||||
_remaining--;
|
||||
completed++;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
bool committed = error is MeshReferenceMutationException
|
||||
{
|
||||
MutationCommitted: true,
|
||||
};
|
||||
if (committed)
|
||||
{
|
||||
entry.Completed = true;
|
||||
_remaining--;
|
||||
completed++;
|
||||
}
|
||||
|
||||
(failures ??= []).Add(
|
||||
new ResourceReleaseFailure(entry.Name, error, committed));
|
||||
}
|
||||
finally
|
||||
{
|
||||
entry.Running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_advancing = false;
|
||||
}
|
||||
|
||||
return new ResourceReleaseAttempt(
|
||||
attempted,
|
||||
completed,
|
||||
failures ?? []);
|
||||
}
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public Entry(string name, Action release)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("A resource-release stage needs a name.", nameof(name));
|
||||
ArgumentNullException.ThrowIfNull(release);
|
||||
Name = name;
|
||||
Release = release;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public Action Release { get; }
|
||||
public bool Completed { get; set; }
|
||||
public bool Running { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct ResourceReleaseFailure(
|
||||
string Stage,
|
||||
Exception Error,
|
||||
bool MutationCommitted);
|
||||
|
||||
internal readonly record struct ResourceReleaseAttempt(
|
||||
int AttemptedCount,
|
||||
int CompletedCount,
|
||||
IReadOnlyList<ResourceReleaseFailure> Failures)
|
||||
{
|
||||
public bool HasFailures => Failures.Count != 0;
|
||||
|
||||
public AggregateException ToException(string message) =>
|
||||
new(
|
||||
message,
|
||||
Failures.Select(failure =>
|
||||
new InvalidOperationException(
|
||||
$"Resource-release stage '{failure.Stage}' failed "
|
||||
+ (failure.MutationCommitted
|
||||
? "after committing its mutation."
|
||||
: "before committing its mutation."),
|
||||
failure.Error)));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue