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."); } }