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:
Erik 2026-07-18 08:14:05 +02:00
parent ec1bb19609
commit 6b0472ee32
14 changed files with 1083 additions and 34 deletions

View file

@ -0,0 +1,128 @@
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<string>();
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);
}
[Fact]
public void Flush_EqualDistancePreservesSubmissionOrderAcrossRenderers()
{
var log = new List<string>();
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<string>();
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<string>();
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 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 sealed class RecordingSource(string name, List<string> log) : IRetailAlphaDrawSource
{
public List<int> BatchSizes { get; } = new();
public int ResetCount { get; private set; }
public void DrawAlphaBatch(ReadOnlySpan<int> tokens)
{
BatchSizes.Add(tokens.Length);
foreach (int token in tokens)
log.Add($"{name}:{token}");
}
public void ResetAlphaSubmissions() => ResetCount++;
}
}