using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.Tests.Rendering; public sealed class RetailAlphaQueueTests { [Fact] public void Flush_InterleavesRenderersInDescendingCyptOrder() { var log = new List(); var objects = new RecordingSource("object", log); var particles = new RecordingSource("particle", log); var queue = new RetailAlphaQueue(); queue.BeginFrame(); queue.Submit(objects, token: 0, viewerDistance: 10f); // near crystal queue.Submit(particles, token: 0, viewerDistance: 30f); // far smoke queue.Submit(objects, token: 1, viewerDistance: 25f); queue.Submit(particles, token: 1, viewerDistance: 20f); queue.Submit(objects, token: 2, viewerDistance: 5f); queue.EndFrame(); Assert.Equal( new[] { "particle:0", "object:1", "particle:1", "object:0", "object:2", }, log); Assert.Equal(1, objects.ResetCount); Assert.Equal(1, particles.ResetCount); Assert.Equal(1, objects.PrepareCount); Assert.Equal(1, particles.PrepareCount); } [Fact] public void Flush_EqualDistancePreservesSubmissionOrderAcrossRenderers() { var log = new List(); var objects = new RecordingSource("object", log); var particles = new RecordingSource("particle", log); var queue = new RetailAlphaQueue(); queue.BeginFrame(); queue.Submit(objects, 7, 12f); queue.Submit(particles, 4, 12f); queue.Submit(objects, 8, 12f); queue.EndFrame(); Assert.Equal(new[] { "object:7", "particle:4", "object:8" }, log); } [Fact] public void Flush_KeepsFrameOpenForPostDepthClearScope() { var log = new List(); var source = new RecordingSource("alpha", log); var queue = new RetailAlphaQueue(); queue.BeginFrame(); queue.Submit(source, 1, 8f); queue.Flush(); Assert.True(queue.IsCollecting); Assert.Equal(0, queue.PendingCount); queue.Submit(source, 2, 3f); queue.EndFrame(); Assert.False(queue.IsCollecting); Assert.Equal(new[] { "alpha:1", "alpha:2" }, log); Assert.Equal(2, source.ResetCount); } [Fact] public void Flush_BatchesOnlyAdjacentEntriesFromSameRenderer() { var log = new List(); var objects = new RecordingSource("object", log); var particles = new RecordingSource("particle", log); var queue = new RetailAlphaQueue(); queue.BeginFrame(); queue.Submit(objects, 0, 40f); queue.Submit(objects, 1, 35f); queue.Submit(particles, 0, 30f); queue.Submit(objects, 2, 25f); queue.EndFrame(); Assert.Equal(new[] { 2, 1 }, objects.BatchSizes); Assert.Equal(new[] { 1 }, particles.BatchSizes); } [Fact] public void Flush_LargeInputMatchesStableDescendingRetailOrder() { var log = new List(); var source = new RecordingSource("alpha", log); var queue = new RetailAlphaQueue(); var random = new Random(0xAC2013); float[] distances = new float[4_096]; queue.BeginFrame(); for (int i = 0; i < distances.Length; i++) { distances[i] = i % 127 switch { 0 => float.NaN, 1 => float.PositiveInfinity, 2 => -0f, 3 => -1f, _ => random.Next(0, 64), }; queue.Submit(source, i, distances[i]); } queue.EndFrame(); int[] expected = Enumerable.Range(0, distances.Length) .OrderByDescending(i => NormalizeForReference(distances[i])) .ToArray(); Assert.Equal(expected, source.PreparedTokens); } [Fact] public void Flush_ExtremeFloatKeysMatchNormalizedRetailOrder() { var log = new List(); var source = new RecordingSource("alpha", log); var queue = new RetailAlphaQueue(); float[] distances = [ 1f, float.BitIncrement(1f), float.BitDecrement(1f), float.Epsilon, float.MaxValue, 0f, -0f, float.NaN, float.PositiveInfinity, float.NegativeInfinity, -float.Epsilon, 1f, ]; queue.BeginFrame(); for (int i = 0; i < distances.Length; i++) queue.Submit(source, i, distances[i]); queue.EndFrame(); int[] expected = Enumerable.Range(0, distances.Length) .OrderByDescending(i => NormalizeForReference(distances[i])) .ToArray(); Assert.Equal(expected, source.PreparedTokens); } [Fact] public void Flush_EqualDistanceOrderStartsFreshInEachFrameScope() { var log = new List(); var source = new RecordingSource("alpha", log); var queue = new RetailAlphaQueue(); queue.BeginFrame(); queue.Submit(source, 1, 12f); queue.Submit(source, 2, 12f); queue.EndFrame(); queue.BeginFrame(); queue.Submit(source, 9, 12f); queue.Submit(source, 8, 12f); queue.EndFrame(); Assert.Equal( ["alpha:1", "alpha:2", "alpha:9", "alpha:8"], log); } [Fact] public void ComputeViewerDistance_UsesTransformedGfxSortCenter() { // Retail CPhysicsPart::UpdateViewerDistance 0x0050E030 applies scale, // orientation, and translation to gfxobj.sort_center before CYpt. var model = Matrix4x4.CreateScale(2f) * Matrix4x4.CreateRotationZ(MathF.PI / 2f) * Matrix4x4.CreateTranslation(10f, 20f, 30f); Vector3 localSortCenter = new(1f, 0f, 0f); Vector3 camera = new(10f, 20f, 30f); float cypt = RetailAlphaOrdering.ComputeViewerDistance( localSortCenter, model, camera); Assert.Equal(2f, cypt, precision: 5); } private static float NormalizeForReference(float value) => float.IsFinite(value) && value > 0f ? value : 0f; private sealed class RecordingSource(string name, List log) : IRetailAlphaDrawSource { public List BatchSizes { get; } = new(); public int ResetCount { get; private set; } public int PrepareCount { get; private set; } public IReadOnlyList PreparedTokens => _prepared; private int[] _prepared = []; public void PrepareAlphaDraws(ReadOnlySpan tokens) { PrepareCount++; _prepared = tokens.ToArray(); } public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) { BatchSizes.Add(drawCount); for (int i = 0; i < drawCount; i++) log.Add($"{name}:{_prepared[firstPreparedDraw + i]}"); } public void ResetAlphaSubmissions() => ResetCount++; } }