fix(rendering): port retail shared alpha list

Queue translucent world GfxObj batches and scene particles in one stable far-to-near stream using transformed DAT sort centers, then drain it at retail's landscape and final-world boundaries. Preserve authored blend, cull, lighting, opacity, and adjacent-only batching so particles behind lifestones are composited through the crystal instead of overpainting it.

Release build succeeds and all 5,914 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-18 08:14:05 +02:00
parent ec1bb19609
commit 6b0472ee32
14 changed files with 1083 additions and 34 deletions

View file

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.Rendering;
internal static class RetailAlphaOrdering
{
/// <summary>
/// Retail <c>CPhysicsPart::UpdateViewerDistance</c> 0x0050E030:
/// transform the GfxObj's authored sort center through the part draw frame,
/// then store its Euclidean distance from the viewer as <c>CYpt</c>.
/// </summary>
public static float ComputeViewerDistance(
Vector3 localSortCenter,
Matrix4x4 model,
Vector3 cameraWorldPosition)
=> Vector3.Distance(Vector3.Transform(localSortCenter, model), cameraWorldPosition);
}
/// <summary>
/// Renderer-owned half of retail's delayed alpha list. A source keeps the
/// immutable payload addressed by each token until the queue flushes, then
/// releases the payload when <see cref="ResetAlphaSubmissions"/> is called.
/// </summary>
internal interface IRetailAlphaDrawSource
{
void DrawAlphaBatch(ReadOnlySpan<int> tokens);
void ResetAlphaSubmissions();
}
internal readonly record struct RetailAlphaSubmission(
IRetailAlphaDrawSource Source,
int Token,
float ViewerDistance,
long Sequence);
/// <summary>
/// Frame-scoped port of retail's shared <c>D3DPolyRender</c> alpha list.
/// Scene-particle parts and ordinary GfxObj parts enter one stable,
/// far-to-near stream instead of being composited in renderer-local passes.
///
/// Retail oracle:
/// <list type="bullet">
/// <item><c>CShadowPart::insertion_sort</c> 0x006B5130 orders all cell parts by
/// <c>CPhysicsPart::CYpt</c>.</item>
/// <item><c>D3DPolyRender::AddMeshToAlphaList</c> 0x0059C230 appends delayed
/// surface subsets in that established order.</item>
/// <item><c>D3DPolyRender::FlushAlphaList</c> 0x0059D2E0 drains without another
/// material/renderer sort.</item>
/// </list>
/// </summary>
internal sealed class RetailAlphaQueue
{
private readonly List<RetailAlphaSubmission> _submissions = new(256);
private readonly List<IRetailAlphaDrawSource> _sources = new(4);
private int[] _tokenScratch = new int[256];
private long _nextSequence;
public bool IsCollecting { get; private set; }
internal int PendingCount => _submissions.Count;
public void BeginFrame()
{
if (IsCollecting)
throw new InvalidOperationException("Retail alpha frame is already active.");
if (_submissions.Count != 0 || _sources.Count != 0)
throw new InvalidOperationException("Retail alpha queue retained payload outside a frame.");
_nextSequence = 0;
IsCollecting = true;
}
public void Submit(IRetailAlphaDrawSource source, int token, float viewerDistance)
{
ArgumentNullException.ThrowIfNull(source);
if (!IsCollecting)
throw new InvalidOperationException("Retail alpha submission requires an active frame.");
if (token < 0)
throw new ArgumentOutOfRangeException(nameof(token));
_submissions.Add(new RetailAlphaSubmission(
source,
token,
NormalizeDistance(viewerDistance),
_nextSequence++));
for (int i = 0; i < _sources.Count; i++)
if (ReferenceEquals(_sources[i], source))
return;
_sources.Add(source);
}
/// <summary>
/// Drains the current retail alpha scope and keeps the frame open so the
/// caller can collect the post-depth-clear scope. Only adjacent entries
/// from one renderer are handed over as a batch; the queue never groups by
/// renderer across another entry and therefore cannot alter compositing.
/// </summary>
public void Flush()
{
if (!IsCollecting)
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
try
{
if (_submissions.Count == 0)
return;
_submissions.Sort(CompareRetailOrder);
EnsureTokenCapacity(_submissions.Count);
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;
for (int i = 0; i < count; i++)
_tokenScratch[i] = _submissions[start + i].Token;
source.DrawAlphaBatch(_tokenScratch.AsSpan(0, count));
start = end;
}
}
finally
{
for (int i = 0; i < _sources.Count; i++)
_sources[i].ResetAlphaSubmissions();
_sources.Clear();
_submissions.Clear();
}
}
public void EndFrame()
{
if (!IsCollecting)
throw new InvalidOperationException("Retail alpha frame is not active.");
try
{
Flush();
}
finally
{
IsCollecting = false;
}
}
private static int CompareRetailOrder(RetailAlphaSubmission left, RetailAlphaSubmission right)
{
// CShadowPart::insertion_sort 0x006B5130 produces descending CYpt
// (far-to-near). Sequence makes the List.Sort result stable for equal
// distances, preserving part/surface append order like retail.
int distance = right.ViewerDistance.CompareTo(left.ViewerDistance);
return distance != 0 ? distance : left.Sequence.CompareTo(right.Sequence);
}
private static float NormalizeDistance(float value)
=> float.IsFinite(value) && value >= 0f ? value : 0f;
private void EnsureTokenCapacity(int count)
{
if (_tokenScratch.Length >= count)
return;
Array.Resize(ref _tokenScratch, count + 256);
}
}