using System.Reflection;
using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering;
///
/// S4-c2: 's two-list FIFO rewrite. Every test
/// below has a mutation check recorded in its own doc comment (or the S4-c2
/// commit body) proving it fails without the change it pins.
///
public sealed class RetailAlphaQueueTests
{
/// Mutation check: reverting TryAppend/Flush to the
/// old distance-sorted single queue (submitting these same three entries
/// through the old Submit(source, token, viewerDistance) API with
/// distances 30/10/5 — far to near) drains far-to-near
/// (alpha:0, alpha:1, alpha:2), NOT append order — this assertion
/// fails against that old behavior.
[Fact]
public void Flush_FifoBeatsReversedDistanceInOneCell()
{
var log = new List();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
// "Reversed distance": entry 0 is submitted first but would be
// farthest under the deleted distance model; entry 2 nearest.
// FIFO means append order alone decides replay order now.
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 0, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 1, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 2, false));
queue.EndFrame();
Assert.Equal(new[] { "alpha:0", "alpha:1", "alpha:2" }, log);
}
/// Mutation check: sorting entries by any key (even a stable
/// one) before draining, instead of never sorting at all, cannot be
/// distinguished from FIFO for a single source's own append order — so
/// this test interleaves TWO sources with equal claim to "first" and
/// pins that neither source's internal order nor a materialGroup-style
/// regrouping can reorder them: object and particle entries must stay in
/// the exact submission interleave.
[Fact]
public void Flush_EqualPrioritySourcesPreserveSubmissionInterleave()
{
var log = new List();
var objects = new RecordingSource("object", log);
var particles = new RecordingSource("particle", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, objects, 7, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particles, 4, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, objects, 8, false));
queue.EndFrame();
Assert.Equal(new[] { "object:7", "particle:4", "object:8" }, log);
}
/// Two "cells" (two BeginFrame/EndFrame scopes) where a
/// GLOBAL distance sort across both scopes would disagree with per-scope
/// traversal order: cell A submits a "far" entry then a "near" one, cell
/// B (a later scope) submits a "very near" entry. A global sort by
/// distance would put cell B's very-near entry ahead of BOTH of cell A's
/// entries; per-scope FIFO traversal (what this test pins) keeps cell A
/// entirely before cell B regardless of any distance value that would
/// have been attached. Mutation check: sorting the combined per-scope
/// output by a synthetic "distance" derived from token order (as the old
/// queue's radix sort effectively encoded via ViewerDistance) would
/// still pass this test since FIFO happens to coincide with ascending
/// token order here — so the real proof is
/// above, which
/// this test complements by proving traversal never leaks across scope
/// boundaries.
[Fact]
public void Flush_TwoScopesNeverInterleaveRegardlessOfGlobalOrder()
{
var log = new List();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 1, false)); // "far"
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 2, false)); // "near"
queue.EndFrame();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 9, false)); // "very near"
queue.EndFrame();
Assert.Equal(new[] { "alpha:1", "alpha:2", "alpha:9" }, log);
}
/// Particle, object, and (conceptually) transparent-cell
/// content overlapping in one scope: three distinct sources interleave
/// in submission order. Mutation check: grouping by source (drawing all
/// of one source's entries before any of another's, e.g. "prepare
/// completely per source then draw all its batches") instead of walking
/// the combined append order would produce
/// object:1,object:2,particle:1,cell:1 — this assertion fails
/// against that grouping.
[Fact]
public void Flush_ParticleObjectAndCellSourcesOverlapInSubmissionOrder()
{
var log = new List();
var objects = new RecordingSource("object", log);
var particles = new RecordingSource("particle", log);
var cellShells = new RecordingSource("cell", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, objects, 1, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particles, 1, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellShells, 1, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, objects, 2, false));
queue.EndFrame();
Assert.Equal(new[] { "object:1", "particle:1", "cell:1", "object:2" }, log);
}
/// Retail RenderDeviceD3D::DrawBuilding's own
/// FlushAlphaList(0f). Mutation check: passing any nonzero
/// threshold here (e.g. leaving the old hardcoded 0f-only Flush()
/// signature but silently routing DrawBuilding through the 0.75f valve
/// instead) would make this single low-count entry a no-op — the
/// assertion that it drained would fail.
[Fact]
public void Flush_DrawBuildingSiteAtZeroThresholdAlwaysDrains()
{
var log = new List();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 1, false));
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
Assert.Equal(new[] { "alpha:1" }, log);
Assert.Equal(0, queue.PendingCount);
Assert.True(queue.IsCollecting);
queue.EndFrame();
}
/// Pre-clear partial flush (frame stays open, keeping later
/// content) then the final end-of-frame flush drains the rest. Mutation
/// check: an EndFrame that forgets to flush at all (or a
/// Flush that clears )
/// would leave alpha:2 undrained or the frame permanently open —
/// both assertions below fail against that bug.
[Fact]
public void Flush_PreClearThenFinalFlushBothDrainInOrder()
{
var log = new List();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 1, false));
queue.Flush(RetailAlphaFlushSite.LandscapeFlush, 0f);
Assert.True(queue.IsCollecting);
Assert.Equal(0, queue.PendingCount);
Assert.Equal(new[] { "alpha:1" }, log);
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 2, false));
queue.EndFrame();
Assert.False(queue.IsCollecting);
Assert.Equal(new[] { "alpha:1", "alpha:2" }, log);
Assert.Equal(2, source.ResetCount);
}
///
/// Ghidra-verified 2026-09-04 boundary
/// (D3DPolyRender::FlushAlphaList @0x0059d2e0): the early return
/// fires only when BOTH counts are STRICTLY below threshold * 3000.
/// At exactly 2250 (0.75 * 3000) the ALPHA count is NOT strictly less
/// than 2250, so the drain proceeds. Mutation check: using
/// <= instead of < for the no-op comparison makes
/// this exact-2250 case a no-op — the drained-count assertion (2250, not
/// 0) fails against that mutation.
[Fact]
public void Flush_SortCellExitValveDrainsExactlyAtTwoThousandTwoHundredFifty()
{
var log = new List();
var source = new CountingSource();
var queue = new RetailAlphaQueue();
queue.BeginFrame();
for (int i = 0; i < 2250; i++)
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, i, false));
queue.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
Assert.Equal(0, queue.PendingCount);
Assert.Equal(2250, source.LastDrawCount);
Assert.Equal(1, source.ResetCount);
queue.AbortFrame();
}
/// The complement of the boundary test above: one entry BELOW
/// 2250 in both lists is a true no-op (both lists left exactly as they
/// were). Mutation check: a valve that drains "at or above 2249" (an
/// off-by-one on the threshold constant, not just the comparison
/// operator) would drain here too — the assertion that the entry is
/// STILL pending and nothing was drawn fails against that mutation.
[Fact]
public void Flush_SortCellExitValveIsANoOpOneBelowTheBoundary()
{
var log = new List();
var source = new CountingSource();
var queue = new RetailAlphaQueue();
queue.BeginFrame();
for (int i = 0; i < 2249; i++)
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, i, false));
queue.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
Assert.Equal(2249, queue.PendingCount);
Assert.Equal(0, source.PrepareCount);
Assert.Equal(0, source.ResetCount);
queue.AbortFrame();
}
/// Alternating CLIP/ALPHA appends from ONE source must still
/// batch as one contiguous run per Vulkan draw call across the
/// CLIP-then-ALPHA boundary (retail draws CLIP fully, then ALPHA fully —
/// nothing about a shared source spanning that boundary changes visual
/// order, since CLIP entries always precede all ALPHA entries anyway).
/// A second source's single CLIP entry, interposed between the first
/// source's CLIP and ALPHA entries, must split that run into two
/// batches. Mutation check: preparing/drawing CLIP and ALPHA as two
/// fully independent per-list passes (never combining a source's tokens
/// across both lists into one prepare call) would call
/// PrepareAlphaDraws twice for the shared source instead of once —
/// PrepareCount asserted at 1 fails against that mutation.
[Fact]
public void Flush_BatchesAdjacentSameSourceEntriesAcrossTheClipAlphaBoundary()
{
var log = new List();
var shared = new RecordingSource("shared", log);
var other = new RecordingSource("other", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
Assert.True(queue.TryAppend(RetailAlphaList.Clip, shared, 100, false));
Assert.True(queue.TryAppend(RetailAlphaList.Clip, other, 200, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, shared, 300, false));
queue.EndFrame();
// Drain order: CLIP fully (shared:100, other:200) then ALPHA fully
// (shared:300) — shared's ALPHA entry is adjacent to other's CLIP
// entry in the combined sequence, so shared gets TWO batches (its
// own CLIP run of one, then its ALPHA run of one) while other gets
// one.
Assert.Equal(new[] { "shared:100", "other:200", "shared:300" }, log);
Assert.Equal(new[] { 1, 1 }, shared.BatchSizes);
Assert.Equal(new[] { 1 }, other.BatchSizes);
Assert.Equal(1, shared.PrepareCount);
Assert.Equal(1, other.PrepareCount);
}
///
/// D3DPolyRender::AddMeshToAlphaList @0x0059c230 (Ghidra-verified
/// 2026-09-04): append returns once the target
/// list already holds (3000)
/// entries; the subset is DROPPED, no recovery. Mutation check: growing
/// the backing list instead of rejecting the 3001st append would make
/// TryAppend return and
/// PendingCount read 3001 — both assertions fail against that
/// mutation.
[Fact]
public void TryAppend_CapacityOverflowDropsTheSubsetWithoutRecovery()
{
var source = new CountingSource();
var queue = new RetailAlphaQueue();
queue.BeginFrame();
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
Assert.True(queue.TryAppend(RetailAlphaList.Clip, source, i, false));
bool overflowed = queue.TryAppend(RetailAlphaList.Clip, source, 3000, false);
Assert.False(overflowed);
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.ClipCount);
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.PendingCount);
queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
// The dropped 3001st token (3000) never reaches the source at all —
// only the 3000 accepted entries drew.
Assert.Equal(RetailAlphaQueue.ListCapacity, source.LastDrawCount);
}
/// The ALPHA list has its own independent capacity — filling
/// CLIP to capacity must not affect ALPHA appends.
[Fact]
public void TryAppend_ClipAndAlphaCapacitiesAreIndependent()
{
var source = new CountingSource();
var queue = new RetailAlphaQueue();
queue.BeginFrame();
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
Assert.True(queue.TryAppend(RetailAlphaList.Clip, source, i, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, 9999, false));
Assert.Equal(1, queue.AlphaCount);
queue.AbortFrame();
}
/// The first entry appended to a list after it was last drained
/// is flagged IsFirstForList; later entries in the same
/// uninterrupted run are not. Mutation check: always setting the flag
/// true (or always false) fails this exact sequence assertion.
[Fact]
public void TryAppend_FlagsOnlyTheFirstEntrySinceTheLastDrain()
{
var log = new List();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
FieldInfo alphaField = typeof(RetailAlphaQueue).GetField(
"_alpha", BindingFlags.NonPublic | BindingFlags.Instance)!;
queue.BeginFrame();
queue.TryAppend(RetailAlphaList.Alpha, source, 1, false);
queue.TryAppend(RetailAlphaList.Alpha, source, 2, false);
// Inspect BEFORE flushing: two entries in the SAME list snapshot,
// discriminating true (first) from false (second) — checking only
// the post-flush single-survivor list (as an earlier draft of this
// test did) is vacuous, since a one-element list is trivially
// "first" whether or not the flag logic is correct.
var beforeFlush = (List)alphaField.GetValue(queue)!;
Assert.Equal(2, beforeFlush.Count);
Assert.True(beforeFlush[0].IsFirstForList);
Assert.False(beforeFlush[1].IsFirstForList);
queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
queue.TryAppend(RetailAlphaList.Alpha, source, 3, false);
var afterDrain = (List)alphaField.GetValue(queue)!;
Assert.Single(afterDrain);
Assert.True(afterDrain[0].IsFirstForList);
queue.AbortFrame();
}
[Fact]
public void RetainedScratchConvergesAfterAOneScopeSpike()
{
const int budgetBytes = 128 * 1024;
var source = new CountingSource();
var queue = new RetailAlphaQueue(budgetBytes);
queue.BeginFrame();
for (int i = 0; i < 8_192; i++)
queue.TryAppend(i % 2 == 0 ? RetailAlphaList.Clip : RetailAlphaList.Alpha, source, i, false);
queue.EndFrame();
Assert.True(queue.RetainedScratchBytes > budgetBytes);
for (int i = 0; i < 3; i++)
{
queue.BeginFrame();
queue.EndFrame();
}
Assert.True(queue.RetainedScratchBytes <= budgetBytes);
Assert.False(queue.IsCollecting);
Assert.Equal(0, queue.PendingCount);
}
[Fact]
public void AbortFrame_DiscardsPayloadAndAllowsTheNextFrameToRender()
{
var log = new List();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.TryAppend(RetailAlphaList.Alpha, source, 1, false);
queue.AbortFrame();
Assert.False(queue.IsCollecting);
Assert.Equal(0, queue.PendingCount);
Assert.Empty(log);
Assert.Equal(1, source.ResetCount);
queue.BeginFrame();
queue.TryAppend(RetailAlphaList.Alpha, source, 2, false);
queue.EndFrame();
Assert.Equal(new[] { "alpha:2" }, log);
Assert.Equal(2, source.ResetCount);
}
[Fact]
public void EndFrame_DrawAndResetFailuresPreserveThePrimaryFailureAndClearTheFrame()
{
var drawSource = new FailureSource("draw failed", "first reset failed");
var secondSource = new FailureSource(null, null);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.TryAppend(RetailAlphaList.Alpha, drawSource, 1, false);
queue.TryAppend(RetailAlphaList.Alpha, secondSource, 2, false);
AggregateException failure = Assert.Throws(queue.EndFrame);
Assert.Collection(
failure.InnerExceptions,
error => Assert.Equal("draw failed", error.Message),
error => Assert.Equal("first reset failed", error.Message));
Assert.Equal(1, drawSource.ResetCount);
Assert.Equal(1, secondSource.ResetCount);
Assert.Equal(0, queue.PendingCount);
Assert.False(queue.IsCollecting);
}
[Fact]
public void EndFrame_MultipleResetFailuresAttemptEverySourceAndClearTheFrame()
{
var first = new FailureSource(null, "first reset failed");
var second = new FailureSource(null, "second reset failed");
var queue = new RetailAlphaQueue();
queue.BeginFrame();
queue.TryAppend(RetailAlphaList.Alpha, first, 1, false);
queue.TryAppend(RetailAlphaList.Alpha, second, 2, false);
AggregateException failure = Assert.Throws(queue.EndFrame);
Assert.Collection(
failure.InnerExceptions,
error => Assert.Equal("first reset failed", error.Message),
error => Assert.Equal("second reset failed", error.Message));
Assert.Equal(1, first.ResetCount);
Assert.Equal(1, second.ResetCount);
Assert.Equal(0, queue.PendingCount);
Assert.False(queue.IsCollecting);
}
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; }
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++;
}
/// A source that only counts — used for the high-volume
/// capacity/threshold tests where recording every token as a string
/// would be wasted allocation.
private sealed class CountingSource : IRetailAlphaDrawSource
{
public int PrepareCount { get; private set; }
public int ResetCount { get; private set; }
public int LastDrawCount { get; private set; }
public void PrepareAlphaDraws(ReadOnlySpan tokens) => PrepareCount++;
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) =>
LastDrawCount = drawCount;
public void ResetAlphaSubmissions() => ResetCount++;
}
private sealed class FailureSource(
string? drawFailure,
string? resetFailure) : IRetailAlphaDrawSource
{
public int ResetCount { get; private set; }
public void PrepareAlphaDraws(ReadOnlySpan tokens)
{
}
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
if (drawFailure is not null)
throw new InvalidOperationException(drawFailure);
}
public void ResetAlphaSubmissions()
{
ResetCount++;
if (resetFailure is not null)
throw new InvalidOperationException(resetFailure);
}
}
}