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:
parent
ec1bb19609
commit
6b0472ee32
14 changed files with 1083 additions and 34 deletions
128
tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs
Normal file
128
tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,9 @@ namespace AcDream.App.Tests.Rendering.Wb;
|
|||
public class InstanceGroupClearTests
|
||||
{
|
||||
// #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds
|
||||
// six per-instance parallel lists — Matrices, Slots, LightSets, IndoorFlags,
|
||||
// Opacities, SelectionLighting — appended in lockstep (one entry per drawn instance) every frame. The
|
||||
// seven per-instance parallel lists — Matrices, LocalSortCenters, Slots,
|
||||
// LightSets, IndoorFlags, Opacities, SelectionLighting — appended in lockstep
|
||||
// (one entry per drawn instance) every frame. The
|
||||
// per-frame reset must clear ALL of them. #188 added Opacities but left it out of
|
||||
// the inline clear loop, so it grew one float per instance per frame forever; as
|
||||
// List<float>'s backing array doubled it produced ~128 MB / ~512 MB LOH float[]
|
||||
|
|
@ -19,6 +20,7 @@ public class InstanceGroupClearTests
|
|||
{
|
||||
var grp = new WbDrawDispatcher.InstanceGroup();
|
||||
grp.Matrices.Add(Matrix4x4.Identity);
|
||||
grp.LocalSortCenters.Add(Vector3.Zero);
|
||||
grp.Slots.Add(1u);
|
||||
grp.LightSets.Add(-1);
|
||||
grp.IndoorFlags.Add(0u);
|
||||
|
|
@ -28,6 +30,7 @@ public class InstanceGroupClearTests
|
|||
grp.ClearPerInstanceData();
|
||||
|
||||
Assert.Empty(grp.Matrices);
|
||||
Assert.Empty(grp.LocalSortCenters);
|
||||
Assert.Empty(grp.Slots);
|
||||
Assert.Empty(grp.LightSets);
|
||||
Assert.Empty(grp.IndoorFlags);
|
||||
|
|
|
|||
|
|
@ -413,7 +413,12 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
/// produces under the collector pattern.
|
||||
/// </summary>
|
||||
private static CachedBatch MakeCachedBatch(
|
||||
uint ibo, uint firstIndex, int indexCount, ulong texHandle, Matrix4x4? restPose = null)
|
||||
uint ibo,
|
||||
uint firstIndex,
|
||||
int indexCount,
|
||||
ulong texHandle,
|
||||
Matrix4x4? restPose = null,
|
||||
Vector3? localSortCenter = null)
|
||||
{
|
||||
var key = new GroupKey(
|
||||
Ibo: ibo,
|
||||
|
|
@ -423,7 +428,11 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
BindlessTextureHandle: texHandle,
|
||||
TextureLayer: 0,
|
||||
Translucency: TranslucencyKind.Opaque);
|
||||
return new CachedBatch(key, texHandle, restPose ?? Matrix4x4.Identity);
|
||||
return new CachedBatch(
|
||||
key,
|
||||
texHandle,
|
||||
restPose ?? Matrix4x4.Identity,
|
||||
localSortCenter ?? Vector3.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -479,7 +488,8 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
// Production code: this is the !isAnimated && _cache.TryGet branch
|
||||
// at the top of the per-entity loop body in Draw.
|
||||
var groups = new Dictionary<GroupKey, List<Matrix4x4>>();
|
||||
void AppendInstance(GroupKey k, Matrix4x4 m)
|
||||
var sortCenters = new List<Vector3>();
|
||||
void AppendInstance(GroupKey k, Matrix4x4 m, Vector3 localSortCenter)
|
||||
{
|
||||
if (!groups.TryGetValue(k, out var list))
|
||||
{
|
||||
|
|
@ -487,6 +497,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
groups[k] = list;
|
||||
}
|
||||
list.Add(m);
|
||||
sortCenters.Add(localSortCenter);
|
||||
}
|
||||
|
||||
Assert.True(cache.TryGet(EntityId, LandblockId, out var entryHit));
|
||||
|
|
@ -508,6 +519,35 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
// appended matrix must equal entityWorld.
|
||||
foreach (var (_, list) in groups)
|
||||
Assert.Equal(entityWorld, list[0]);
|
||||
Assert.All(sortCenters, center => Assert.Equal(Vector3.Zero, center));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyCacheHit_PreservesAuthoredSortCenter()
|
||||
{
|
||||
Vector3 authoredCenter = new(1.25f, -2.5f, 7.75f);
|
||||
var entry = new EntityCacheEntry
|
||||
{
|
||||
EntityId = 100,
|
||||
LandblockHint = 0xA9B40000u,
|
||||
Batches =
|
||||
[
|
||||
MakeCachedBatch(
|
||||
ibo: 1,
|
||||
firstIndex: 0,
|
||||
indexCount: 6,
|
||||
texHandle: 0xAA,
|
||||
localSortCenter: authoredCenter),
|
||||
],
|
||||
};
|
||||
Vector3 observedCenter = default;
|
||||
|
||||
WbDrawDispatcher.ApplyCacheHit(
|
||||
entry,
|
||||
Matrix4x4.Identity,
|
||||
(_, _, center) => observedCenter = center);
|
||||
|
||||
Assert.Equal(authoredCenter, observedCenter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -748,7 +788,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
const uint EntityId = 100;
|
||||
const int MeshRefCount = 3;
|
||||
|
||||
void AppendInstance(GroupKey k, Matrix4x4 m)
|
||||
void AppendInstance(GroupKey k, Matrix4x4 m, Vector3 localSortCenter)
|
||||
{
|
||||
if (!groups.TryGetValue(k, out var list))
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue