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:
parent
ec1bb19609
commit
6b0472ee32
14 changed files with 1083 additions and 34 deletions
171
src/AcDream.App/Rendering/RetailAlphaQueue.cs
Normal file
171
src/AcDream.App/Rendering/RetailAlphaQueue.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue