refactor(render): extract world scene frame owner

Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 07:22:09 +02:00
parent 85239fb373
commit 28e1cf8029
25 changed files with 2679 additions and 844 deletions

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering;
@ -56,7 +57,16 @@ internal readonly record struct RetailAlphaSubmission(
/// material/renderer sort.</item>
/// </list>
/// </summary>
internal sealed class RetailAlphaQueue
internal interface IWorldSceneAlphaFrame
{
void BeginFrame();
void EndFrame();
void AbortFrame();
}
internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
{
private readonly List<RetailAlphaSubmission> _submissions = new(256);
private readonly List<IRetailAlphaDrawSource> _sources = new(4);
@ -108,58 +118,93 @@ internal sealed class RetailAlphaQueue
if (!IsCollecting)
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
Exception? drawFailure = null;
List<Exception>? resetFailures = null;
try
{
if (_submissions.Count == 0)
return;
SortRetailOrder();
EnsureTokenCapacity(_submissions.Count);
EnsureSourceCapacity(_sources.Count);
Array.Clear(_sourceDrawOffsets, 0, _sources.Count);
// Prepare each renderer once for this alpha scope. The filtered
// token sequence preserves final retail order for that source, so
// every later adjacent source-run maps to one contiguous prepared
// range without another GPU upload.
for (int sourceIndex = 0; sourceIndex < _sources.Count; sourceIndex++)
if (_submissions.Count > 0)
{
IRetailAlphaDrawSource source = _sources[sourceIndex];
int sourceCount = 0;
for (int i = 0; i < _submissions.Count; i++)
SortRetailOrder();
EnsureTokenCapacity(_submissions.Count);
EnsureSourceCapacity(_sources.Count);
Array.Clear(_sourceDrawOffsets, 0, _sources.Count);
// Prepare each renderer once for this alpha scope. The filtered
// token sequence preserves final retail order for that source, so
// every later adjacent source-run maps to one contiguous prepared
// range without another GPU upload.
for (int sourceIndex = 0; sourceIndex < _sources.Count; sourceIndex++)
{
RetailAlphaSubmission submission = _submissions[i];
if (ReferenceEquals(submission.Source, source))
_tokenScratch[sourceCount++] = submission.Token;
IRetailAlphaDrawSource source = _sources[sourceIndex];
int sourceCount = 0;
for (int i = 0; i < _submissions.Count; i++)
{
RetailAlphaSubmission submission = _submissions[i];
if (ReferenceEquals(submission.Source, source))
_tokenScratch[sourceCount++] = submission.Token;
}
source.PrepareAlphaDraws(_tokenScratch.AsSpan(0, sourceCount));
}
source.PrepareAlphaDraws(_tokenScratch.AsSpan(0, sourceCount));
}
int start = 0;
while (start < _submissions.Count)
{
IRetailAlphaDrawSource source = _submissions[start].Source;
int end = start + 1;
while (end < _submissions.Count
&& ReferenceEquals(_submissions[end].Source, source))
end++;
int start = 0;
while (start < _submissions.Count)
{
IRetailAlphaDrawSource source = _submissions[start].Source;
int end = start + 1;
while (end < _submissions.Count
&& ReferenceEquals(_submissions[end].Source, source))
end++;
int count = end - start;
int sourceIndex = FindSourceIndex(source);
int firstPreparedDraw = _sourceDrawOffsets[sourceIndex];
source.DrawPreparedAlphaBatch(firstPreparedDraw, count);
_sourceDrawOffsets[sourceIndex] += count;
start = end;
int count = end - start;
int sourceIndex = FindSourceIndex(source);
int firstPreparedDraw = _sourceDrawOffsets[sourceIndex];
source.DrawPreparedAlphaBatch(firstPreparedDraw, count);
_sourceDrawOffsets[sourceIndex] += count;
start = end;
}
}
}
catch (Exception error)
{
drawFailure = error;
}
finally
{
for (int i = 0; i < _sources.Count; i++)
_sources[i].ResetAlphaSubmissions();
{
try
{
_sources[i].ResetAlphaSubmissions();
}
catch (Exception error)
{
(resetFailures ??= []).Add(error);
}
}
_sources.Clear();
_submissions.Clear();
}
if (drawFailure is not null)
{
if (resetFailures is { Count: > 0 })
{
resetFailures.Insert(0, drawFailure);
throw new AggregateException(
"Retail alpha drawing failed and its submissions could not be fully reset.",
resetFailures);
}
ExceptionDispatchInfo.Capture(drawFailure).Throw();
}
if (resetFailures is { Count: > 0 })
{
throw new AggregateException(
"Retail alpha submissions could not be fully reset.",
resetFailures);
}
}
public void EndFrame()
@ -177,6 +222,39 @@ internal sealed class RetailAlphaQueue
}
}
/// <summary>
/// Discards an incomplete frame without drawing it. Every source is still
/// told to release its retained submission payload so the next frame begins
/// from the same empty invariant as a successful flush.
/// </summary>
public void AbortFrame()
{
List<Exception>? failures = null;
try
{
for (int i = 0; i < _sources.Count; i++)
{
try
{
_sources[i].ResetAlphaSubmissions();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
}
finally
{
_sources.Clear();
_submissions.Clear();
IsCollecting = false;
}
if (failures is { Count: > 0 })
throw new AggregateException("Retail alpha frame abort failed.", failures);
}
private void SortRetailOrder()
{
int count = _submissions.Count;