using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering;
internal static class RetailAlphaOrdering
{
///
/// Retail CPhysicsPart::UpdateViewerDistance 0x0050E030:
/// transform the GfxObj's authored sort center through the part draw frame,
/// then store its Euclidean distance from the viewer as CYpt.
///
public static float ComputeViewerDistance(
Vector3 localSortCenter,
Matrix4x4 model,
Vector3 cameraWorldPosition)
=> Vector3.Distance(Vector3.Transform(localSortCenter, model), cameraWorldPosition);
}
///
/// 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 is called.
///
internal interface IRetailAlphaDrawSource
{
/// Uploads this source's complete payload for the current sorted
/// alpha scope exactly once. Tokens arrive in final far-to-near order.
void PrepareAlphaDraws(ReadOnlySpan tokens);
/// Draws a contiguous range from the payload prepared above.
void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount);
void ResetAlphaSubmissions();
}
internal readonly record struct RetailAlphaSubmission(
IRetailAlphaDrawSource Source,
int Token,
float ViewerDistance);
///
/// Frame-scoped port of retail's shared D3DPolyRender 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:
///
/// - CShadowPart::insertion_sort 0x006B5130 orders all cell parts by
/// CPhysicsPart::CYpt.
/// - D3DPolyRender::AddMeshToAlphaList 0x0059C230 appends delayed
/// surface subsets in that established order.
/// - D3DPolyRender::FlushAlphaList 0x0059D2E0 drains without another
/// material/renderer sort.
///
///
internal sealed class RetailAlphaQueue
{
private readonly List _submissions = new(256);
private readonly List _sources = new(4);
private int[] _tokenScratch = new int[256];
private int[] _sourceDrawOffsets = new int[4];
private RetailAlphaSubmission[] _sortScratch = new RetailAlphaSubmission[256];
private readonly int[] _radixOffsets = new int[256];
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.");
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)));
for (int i = 0; i < _sources.Count; i++)
if (ReferenceEquals(_sources[i], source))
return;
_sources.Add(source);
}
///
/// 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.
///
public void Flush()
{
if (!IsCollecting)
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
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++)
{
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));
}
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;
}
}
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 void SortRetailOrder()
{
int count = _submissions.Count;
if (count <= 1)
return;
EnsureSortCapacity(count);
Span submissions = CollectionsMarshal.AsSpan(_submissions);
Span scratch = _sortScratch.AsSpan(0, count);
// Positive IEEE-754 float bits sort in the same order as their numeric
// values. Complementing those bits turns an ascending stable radix pass
// into retail's descending CYpt order. Four stable byte passes preserve
// original submission order for equal distances, matching
// CShadowPart::insertion_sort without List.Sort's O(n log n) interface
// comparator overhead in particle-heavy views.
RadixPass(submissions, scratch, shift: 0);
RadixPass(scratch, submissions, shift: 8);
RadixPass(submissions, scratch, shift: 16);
RadixPass(scratch, submissions, shift: 24);
}
private static float NormalizeDistance(float value)
// value > 0 also canonicalizes -0 to +0 so its IEEE sort key joins the
// same stable equal-distance run as ordinary zero.
=> float.IsFinite(value) && value > 0f ? value : 0f;
private void RadixPass(
ReadOnlySpan source,
Span destination,
int shift)
{
Array.Clear(_radixOffsets);
for (int i = 0; i < source.Length; i++)
{
uint key = ~BitConverter.SingleToUInt32Bits(source[i].ViewerDistance);
_radixOffsets[(int)((key >> shift) & 0xFF)]++;
}
int prefix = 0;
for (int bucket = 0; bucket < _radixOffsets.Length; bucket++)
{
int bucketCount = _radixOffsets[bucket];
_radixOffsets[bucket] = prefix;
prefix += bucketCount;
}
for (int i = 0; i < source.Length; i++)
{
RetailAlphaSubmission submission = source[i];
uint key = ~BitConverter.SingleToUInt32Bits(submission.ViewerDistance);
int bucket = (int)((key >> shift) & 0xFF);
destination[_radixOffsets[bucket]++] = submission;
}
}
private void EnsureTokenCapacity(int count)
{
if (_tokenScratch.Length >= count)
return;
Array.Resize(ref _tokenScratch, count + 256);
}
private void EnsureSourceCapacity(int count)
{
if (_sourceDrawOffsets.Length >= count)
return;
Array.Resize(ref _sourceDrawOffsets, count + 4);
}
private void EnsureSortCapacity(int count)
{
if (_sortScratch.Length >= count)
return;
Array.Resize(ref _sortScratch, count + 256);
}
private int FindSourceIndex(IRetailAlphaDrawSource source)
{
for (int i = 0; i < _sources.Count; i++)
if (ReferenceEquals(_sources[i], source))
return i;
throw new InvalidOperationException("Retail alpha source was not registered for this frame.");
}
}