feat(render) Campaign FW3.2a: the walk-to-draw population layer
The piece that turns walk-visited static content into draws, with no production frame wiring (FW3.2b roots the frame): - TryClassifyBatch: ONE shared per-batch classify core (the #426 untextured gate, #188 opacity promotion, texture resolve, foliage classification, in the exact original order) extracted from ClassifyBatches; the classic and packed classifiers now call it - behavior-identical, proven by the full hermetic + InstalledDat + Core Wb suites. - ClassifyEntityForWalk / WalkClassifiedBatch: the per-entity seam yielding per-batch keys + instance data WITHOUT InstanceGroup bucketing, plus the per-part selection data (picking stays alive on the walk path - the survey's unlisted-consumer fix). - WalkStaticStreamPopulator: per-entity walk-ordered opaque appends (under depth Less, opaque order is pixel-relevant only for coplanar surfaces, which retail resolves first-drawn-wins in ITS order - never material-grouped), translucent instances to the SAME RetailAlphaQueue via SubmitWalkAlphaInstance (identical viewer distances; walk-order submission improves retail's tie fidelity), selection parts published per entity. - SubmitOrderedStream now owns _orderedDrawCullModes, retiring the FW2-recorded alpha-scope interleaving constraint; DrawIndirectRangeRhi takes an optional cull array (all existing call sites unchanged). The referee test was verified to FAIL against the old shared-scratch behavior. - WalkDrawStage.OutdoorStatic added for the landscape turn. Suites: full Release build 0 warnings; Walk lane 195/1 skip; hermetic 6,747/0 (the two failures the implementation round reported were transient - both pass in isolation and in the full run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b10ad662b0
commit
81c6531727
9 changed files with 1264 additions and 116 deletions
|
|
@ -0,0 +1,636 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW stage FW3.2a: the walk→draw population layer's data-
|
||||
/// equivalence referee. Covers <see cref="WbDrawDispatcher.ClassifyEntityForWalk"/>
|
||||
/// (the shared per-entity classify seam), <see cref="WalkStaticStreamPopulator"/>
|
||||
/// (opaque → <see cref="OrderedDrawStream"/>, translucent → the alpha queue,
|
||||
/// selection publish), and the FW3.2a own-cull-scratch fix to
|
||||
/// <c>SubmitOrderedStream</c>.
|
||||
/// </summary>
|
||||
public sealed class WalkStaticStreamPopulatorTests
|
||||
{
|
||||
// ── Test doubles ────────────────────────────────────────────────────────
|
||||
|
||||
private sealed class RecordingSelectionSink : IRetailSelectionRenderSink
|
||||
{
|
||||
public readonly List<(uint ServerGuid, uint LocalEntityId, int PartIndex, uint GfxObjId, Matrix4x4 LocalToWorld)>
|
||||
Calls = new();
|
||||
|
||||
public void AddVisiblePart(
|
||||
uint serverGuid, uint localEntityId, int partIndex, uint gfxObjId, Matrix4x4 partWorld) =>
|
||||
Calls.Add((serverGuid, localEntityId, partIndex, gfxObjId, partWorld));
|
||||
}
|
||||
|
||||
// ── Synthetic RenderProjectionRecord construction ──────────────────────
|
||||
|
||||
private static RenderProjectionRecord MakeRecord(
|
||||
uint localEntityId,
|
||||
uint serverGuid,
|
||||
Vector3 position,
|
||||
IReadOnlyList<MeshRef> meshRefs,
|
||||
bool isBuildingShell = false,
|
||||
uint parentCellId = 0u) =>
|
||||
new(
|
||||
Id: RenderProjectionId.FromRaw(localEntityId),
|
||||
ProjectionClass: RenderProjectionClass.OutdoorStatic,
|
||||
OwnerIncarnation: RenderOwnerIncarnation.FromRaw(1),
|
||||
Transform: new RenderTransform(Matrix4x4.CreateTranslation(position)),
|
||||
PreviousTransform: default,
|
||||
MeshSet: default,
|
||||
Material: default,
|
||||
Residency: default,
|
||||
Bounds: default,
|
||||
Flags: RenderProjectionFlags.Draw,
|
||||
DegradeState: default,
|
||||
SortKey: new RenderSortKey(0),
|
||||
DirtyMask: default,
|
||||
Source: new RenderSourceMetadata(
|
||||
LocalEntityId: localEntityId,
|
||||
ServerGuid: serverGuid,
|
||||
SourceId: 0,
|
||||
ParentCellId: parentCellId,
|
||||
EffectCellId: 0,
|
||||
BuildingShellAnchorCellId: 0,
|
||||
TransformFingerprint: default,
|
||||
GeometryFingerprint: default,
|
||||
AppearanceFingerprint: default),
|
||||
EntityPayload: new RenderEntityPayload(
|
||||
MeshRefs: meshRefs,
|
||||
PaletteOverride: null,
|
||||
IsBuildingShell: isBuildingShell));
|
||||
|
||||
private static ObjectRenderBatch MakeBatch(
|
||||
uint surfaceId,
|
||||
TranslucencyKind translucency,
|
||||
uint firstIndex,
|
||||
int baseVertex,
|
||||
int indexCount,
|
||||
uint textureSlotIndex,
|
||||
uint textureLayer = 0,
|
||||
CullMode cullMode = CullMode.CounterClockwise) =>
|
||||
new()
|
||||
{
|
||||
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
|
||||
Translucency = translucency,
|
||||
FirstIndex = firstIndex,
|
||||
BaseVertex = (uint)baseVertex,
|
||||
IndexCount = indexCount,
|
||||
TextureSlot = new GpuTextureSlot(textureSlotIndex),
|
||||
TextureIndex = (int)textureLayer,
|
||||
};
|
||||
|
||||
private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) =>
|
||||
new() { Batches = new List<ObjectRenderBatch>(batches) };
|
||||
|
||||
// ── Reflection seam: ObjectMeshManager owns no test-injection API, and
|
||||
// driving real GPU/GfxObj upload for a unit test is out of this stage's
|
||||
// scope — ObjectRenderData/ObjectRenderBatch are plain settable classes,
|
||||
// so this seeds the manager's private cache directly. ──────────────────
|
||||
|
||||
private static void InjectRenderData(ObjectMeshManager manager, ulong id, ObjectRenderData data)
|
||||
{
|
||||
FieldInfo field = typeof(ObjectMeshManager).GetField(
|
||||
"_renderData", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException(
|
||||
"ObjectMeshManager._renderData field not found — test relies on this exact name.");
|
||||
var dict = (ConcurrentDictionary<ulong, ObjectRenderData>)field.GetValue(manager)!;
|
||||
dict[id] = data;
|
||||
}
|
||||
|
||||
// ── Deliverable 1: ClassifyEntityForWalk data equivalence ─────────────
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_OneOpaqueAndOneTranslucentPart_YieldsBatchesInRecordOrderWithCorrectIsOpaque()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong opaqueGfxObj = 0x0100_0001UL;
|
||||
const ulong alphaGfxObj = 0x0100_0002UL;
|
||||
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
|
||||
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2)));
|
||||
|
||||
var meshRefs = new[]
|
||||
{
|
||||
new MeshRef((uint)opaqueGfxObj, Matrix4x4.CreateTranslation(1, 0, 0)),
|
||||
new MeshRef((uint)alphaGfxObj, Matrix4x4.CreateTranslation(0, 1, 0)),
|
||||
};
|
||||
RenderProjectionRecord record = MakeRecord(
|
||||
localEntityId: 100, serverGuid: 0, position: new Vector3(5, 6, 7), meshRefs);
|
||||
|
||||
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||
fx.Dispatcher.ClassifyEntityForWalk(in record, tupleLandblockId: 0x8C04u, batches, selectionParts);
|
||||
|
||||
Assert.Equal(2, batches.Count);
|
||||
|
||||
WbDrawDispatcher.WalkClassifiedBatch opaque = batches[0];
|
||||
Assert.True(opaque.IsOpaque);
|
||||
Assert.Equal(TranslucencyKind.Opaque, opaque.Key.Translucency);
|
||||
Assert.Equal(0u, opaque.Key.FirstIndex);
|
||||
Assert.Equal(3, opaque.Key.IndexCount);
|
||||
Assert.Equal(1u, opaque.Key.TextureSlot.Index);
|
||||
Assert.Equal(1f, opaque.Alpha);
|
||||
Assert.Equal(meshRefs[0].PartTransform * record.Transform.LocalToWorld, opaque.Transform);
|
||||
|
||||
WbDrawDispatcher.WalkClassifiedBatch translucent = batches[1];
|
||||
Assert.False(translucent.IsOpaque);
|
||||
Assert.Equal(TranslucencyKind.AlphaBlend, translucent.Key.Translucency);
|
||||
Assert.Equal(3u, translucent.Key.FirstIndex);
|
||||
Assert.Equal(6, translucent.Key.IndexCount);
|
||||
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
|
||||
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
|
||||
|
||||
Assert.Equal(2, selectionParts.Count);
|
||||
Assert.Equal(100u, selectionParts[0].LocalEntityId);
|
||||
Assert.Equal(0, selectionParts[0].PartIndex);
|
||||
Assert.Equal((uint)opaqueGfxObj, selectionParts[0].GfxObjId);
|
||||
Assert.Equal(opaque.Transform, selectionParts[0].LocalToWorld);
|
||||
Assert.Equal(1, selectionParts[1].PartIndex);
|
||||
Assert.Equal((uint)alphaGfxObj, selectionParts[1].GfxObjId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyEntityForWalk_SetupComposite_EncodesPartAndSetupPartIndexLikePackedRoute()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong setupGfxObj = 0x1000_0010UL;
|
||||
const ulong trunkGfxObj = 0x0100_0011UL;
|
||||
const ulong leavesGfxObj = 0x0100_0012UL;
|
||||
|
||||
InjectRenderData(fx.Manager, trunkGfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000011u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
InjectRenderData(fx.Manager, leavesGfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000012u, TranslucencyKind.ClipMap, 3, 4, 6, 2)));
|
||||
InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData
|
||||
{
|
||||
IsSetup = true,
|
||||
SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)>
|
||||
{
|
||||
(trunkGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)),
|
||||
(leavesGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)),
|
||||
},
|
||||
});
|
||||
|
||||
var meshRefs = new[] { new MeshRef((uint)setupGfxObj, Matrix4x4.Identity) };
|
||||
RenderProjectionRecord record = MakeRecord(200, 0, Vector3.Zero, meshRefs);
|
||||
|
||||
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||
fx.Dispatcher.ClassifyEntityForWalk(in record, 0x8C04u, batches, selectionParts);
|
||||
|
||||
Assert.Equal(2, batches.Count);
|
||||
Assert.Equal(2, selectionParts.Count);
|
||||
// partIndex=0 (the entity's single top-level MeshRef) << 16 | setupPartIndex.
|
||||
Assert.Equal(0, selectionParts[0].PartIndex);
|
||||
Assert.Equal(1, selectionParts[1].PartIndex);
|
||||
Assert.Equal((uint)trunkGfxObj, selectionParts[0].GfxObjId);
|
||||
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
|
||||
}
|
||||
|
||||
// ── Deliverable 2: WalkStaticStreamPopulator routing ───────────────────
|
||||
|
||||
[Fact]
|
||||
public void PopulateCell_OpaqueBatchAppendsOrderedDrawCommandInRecordOrderWithStageAndCellProvenance()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong gfxObj = 0x0100_0003UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000003u, TranslucencyKind.Opaque, 10, 2, 12, 5)));
|
||||
|
||||
var record = MakeRecord(300, 0, new Vector3(1, 2, 3), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
||||
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
||||
var stream = new OrderedDrawStream();
|
||||
|
||||
populator.PopulateCell(
|
||||
stream, WalkDrawStage.CellStatic, cellId: 0x8C040100u,
|
||||
new[] { record }, tupleLandblockId: 0x8C04u,
|
||||
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(1, stream.Count);
|
||||
Assert.Equal(WalkDrawStage.CellStatic, stream.Stages[0]);
|
||||
Assert.Equal(0x8C040100u, stream.CellIds[0]);
|
||||
Assert.Equal(10u, stream.Keys[0].FirstIndex);
|
||||
Assert.Equal(12, stream.Keys[0].IndexCount);
|
||||
Assert.Equal(1f, stream.Alphas[0]);
|
||||
Assert.Equal(record.Transform.LocalToWorld, stream.Transforms[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateOutdoorStatics_UsesTheOutdoorStaticStage()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const ulong gfxObj = 0x0100_0004UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000004u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
|
||||
var record = MakeRecord(400, 0, Vector3.Zero, new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
||||
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
||||
var stream = new OrderedDrawStream();
|
||||
|
||||
populator.PopulateOutdoorStatics(
|
||||
stream, cellId: 0x8C040000u, new[] { record }, tupleLandblockId: 0x8C04u,
|
||||
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(1, stream.Count);
|
||||
Assert.Equal(WalkDrawStage.OutdoorStatic, stream.Stages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateCell_TranslucentBatchDoesNotAppendToTheStreamAndReachesTheAlphaQueue()
|
||||
{
|
||||
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
||||
const ulong gfxObj = 0x0100_0005UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000005u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
|
||||
|
||||
var record = MakeRecord(500, 0, new Vector3(0, 0, 10), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
||||
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
||||
var stream = new OrderedDrawStream();
|
||||
|
||||
fx.AlphaQueue!.BeginFrame();
|
||||
populator.PopulateCell(
|
||||
stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u,
|
||||
cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(0, stream.Count);
|
||||
Assert.Equal(1, fx.AlphaQueue.PendingCount);
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateCell_PublishesSelectionPartsForEveryClassifiedEntity()
|
||||
{
|
||||
var sink = new RecordingSelectionSink();
|
||||
using var fx = new DispatcherFixture(selectionSink: sink);
|
||||
const ulong gfxObj = 0x0100_0006UL;
|
||||
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||
MakeBatch(0x08000006u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
|
||||
var record = MakeRecord(600, serverGuid: 0x8000_0060u, new Vector3(1, 1, 1),
|
||||
new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) });
|
||||
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
|
||||
var stream = new OrderedDrawStream();
|
||||
|
||||
populator.PopulateCell(
|
||||
stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u,
|
||||
Vector3.Zero, Matrix4x4.Identity);
|
||||
|
||||
var call = Assert.Single(sink.Calls);
|
||||
Assert.Equal(0x8000_0060u, call.ServerGuid);
|
||||
Assert.Equal(600u, call.LocalEntityId);
|
||||
Assert.Equal(0, call.PartIndex);
|
||||
Assert.Equal((uint)gfxObj, call.GfxObjId);
|
||||
Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld);
|
||||
}
|
||||
|
||||
// ── SubmitWalkAlphaInstance: same viewer distance + per-instance data as
|
||||
// DeferTransparentGroups, through the REAL RetailAlphaQueue. ───────────
|
||||
|
||||
[Fact]
|
||||
public void SubmitWalkAlphaInstance_SubmitsTheSameViewerDistanceComputeViewerDistanceWouldProduce()
|
||||
{
|
||||
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
||||
fx.AlphaQueue!.BeginFrame();
|
||||
|
||||
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
|
||||
Vector3 localSortCenter = new(1, 2, 3);
|
||||
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
|
||||
var cameraWorldPosition = Vector3.Zero;
|
||||
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
||||
key, model, ClipSlot: 7, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 1,
|
||||
Alpha: 0.5f, SelectionLighting: new Vector2(0.25f, 0.75f), DetailCategory: 1,
|
||||
IsOpaque: false, LocalSortCenter: localSortCenter);
|
||||
|
||||
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, cameraWorldPosition, Matrix4x4.Identity);
|
||||
|
||||
Assert.Equal(1, fx.AlphaQueue.PendingCount);
|
||||
|
||||
// Same call DeferTransparentGroups makes per instance — independently
|
||||
// computed here so the assertion cannot pass by construction.
|
||||
float expectedDistance = RetailAlphaOrdering.ComputeViewerDistance(
|
||||
localSortCenter, model, cameraWorldPosition);
|
||||
|
||||
FieldInfo submissionsField = typeof(RetailAlphaQueue).GetField(
|
||||
"_submissions", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var submissions = (List<RetailAlphaSubmission>)submissionsField.GetValue(fx.AlphaQueue)!;
|
||||
RetailAlphaSubmission submission = Assert.Single(submissions);
|
||||
Assert.Equal(expectedDistance, submission.ViewerDistance, precision: 4);
|
||||
Assert.Equal(0, submission.Token);
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
|
||||
{
|
||||
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
||||
fx.AlphaQueue!.BeginFrame();
|
||||
|
||||
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
|
||||
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
||||
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
|
||||
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
|
||||
|
||||
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Vector3.Zero, Matrix4x4.Identity);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fx.Dispatcher.SubmitWalkAlphaInstance(
|
||||
in batch, Vector3.Zero, Matrix4x4.CreateTranslation(1, 0, 0)));
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
// ── Deliverable 3: SubmitOrderedStream's own cull scratch ──────────────
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_DoesNotReadOrCorruptTheSharedAlphaCullScratch()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
// Poison the SHARED _drawCullModes scratch the alpha path owns — the
|
||||
// exact array SubmitOrderedStream used to write into before FW3.2a.
|
||||
// Under the OLD shared-scratch behavior this test's second assertion
|
||||
// fails: SubmitOrderedStream's own command overwrites index 0 with
|
||||
// its own cull mode (Clockwise), destroying the alpha path's poison.
|
||||
FieldInfo field = typeof(WbDrawDispatcher).GetField(
|
||||
"_drawCullModes", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
var poisonModes = (CullMode[])field.GetValue(fx.Dispatcher)!;
|
||||
poisonModes[0] = CullMode.None;
|
||||
|
||||
var stream = new OrderedDrawStream();
|
||||
stream.Append(new OrderedDrawCommand(
|
||||
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
|
||||
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
|
||||
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
|
||||
// (1) The ordered submission's OWN recorded cull call reflects the
|
||||
// STREAM's cull mode (Clockwise -> GpuCullMode.Front), not the
|
||||
// poisoned shared array's (None).
|
||||
List<GpuCullMode> cullCalls = [.. fx.Device.Calls.OfType<GpuRecordedCullMode>().Select(c => c.CullMode)];
|
||||
Assert.Equal([GpuCullMode.Front], cullCalls);
|
||||
|
||||
// (2) The shared _drawCullModes scratch is UNTOUCHED — the ordered
|
||||
// path never wrote through it.
|
||||
var afterModes = (CullMode[])field.GetValue(fx.Dispatcher)!;
|
||||
Assert.Equal(CullMode.None, afterModes[0]);
|
||||
}
|
||||
|
||||
// ── Fixture ─────────────────────────────────────────────────────────────
|
||||
|
||||
private readonly struct DrawScope : IDisposable
|
||||
{
|
||||
private readonly IDisposable _publication;
|
||||
private readonly IGpuPassEncoder _pass;
|
||||
|
||||
public DrawScope(IGpuFrame frame, IGpuPassEncoder pass, IDisposable publication)
|
||||
{
|
||||
Frame = frame;
|
||||
_pass = pass;
|
||||
_publication = publication;
|
||||
}
|
||||
|
||||
public IGpuFrame Frame { get; }
|
||||
|
||||
public IGpuPassEncoder Pass => _pass;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_publication.Dispose();
|
||||
_pass.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DispatcherFixture : IDisposable
|
||||
{
|
||||
private readonly WbMeshAdapter _meshAdapter;
|
||||
private readonly TextureCache _textures;
|
||||
|
||||
public DispatcherFixture(
|
||||
bool withAlphaQueue = false,
|
||||
IRetailSelectionRenderSink? selectionSink = null)
|
||||
{
|
||||
Device = new RecordingGpuDevice();
|
||||
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
||||
Scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||
_textures = new TextureCache(Device, new NoopDatReaderWriter());
|
||||
_meshAdapter = new WbMeshAdapter(
|
||||
Device,
|
||||
new NoopDatReaderWriter(),
|
||||
new NullPreparedAssetSource(),
|
||||
NullLogger<WbMeshAdapter>.Instance,
|
||||
Device.Retirement);
|
||||
var entitySpawnAdapter = new EntitySpawnAdapter(
|
||||
_textures,
|
||||
_ => throw new NotSupportedException("Not exercised by these tests."));
|
||||
AlphaQueue = withAlphaQueue ? new RetailAlphaQueue() : null;
|
||||
|
||||
Dispatcher = new WbDrawDispatcher(
|
||||
Device,
|
||||
FrameLifetime,
|
||||
Scope,
|
||||
_textures,
|
||||
_meshAdapter,
|
||||
entitySpawnAdapter,
|
||||
new EntityClassificationCache(),
|
||||
new AcDream.Core.Rendering.TranslucencyFadeManager(),
|
||||
selectionSink: selectionSink,
|
||||
alphaQueue: AlphaQueue);
|
||||
}
|
||||
|
||||
public RecordingGpuDevice Device { get; }
|
||||
|
||||
public GpuDeviceFrameLifetime FrameLifetime { get; }
|
||||
|
||||
public VulkanWorldPassScope Scope { get; }
|
||||
|
||||
public WbDrawDispatcher Dispatcher { get; }
|
||||
|
||||
public RetailAlphaQueue? AlphaQueue { get; }
|
||||
|
||||
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
||||
|
||||
public DrawScope BeginDraw()
|
||||
{
|
||||
FrameLifetime.BeginFrame();
|
||||
IGpuFrame frame = FrameLifetime.CurrentFrame!;
|
||||
IGpuPassEncoder pass = frame.BeginPass(
|
||||
GpuPassDescription.BackbufferClear(
|
||||
"fw3-2a-walk-populator-test", Vector4.Zero, sampleCount: 1));
|
||||
IDisposable publication = Scope.Publish(pass);
|
||||
Device.Clear();
|
||||
return new DrawScope(frame, pass, publication);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispatcher.Dispose();
|
||||
_meshAdapter.Dispose();
|
||||
_textures.Dispose();
|
||||
Device.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||
{
|
||||
public PreparedAssetSourceStats Stats => default;
|
||||
|
||||
public CacheStats DecodedTextureCacheStats => default;
|
||||
|
||||
public PreparedAssetPresence Probe(
|
||||
AcDream.Content.Pak.PakAssetType type,
|
||||
uint sourceFileId) =>
|
||||
PreparedAssetPresence.Missing;
|
||||
|
||||
public PreparedAssetReadResult Read(
|
||||
in PreparedAssetRequest request,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
PreparedAssetReadResult.Missing;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopDatReaderWriter : IDatReaderWriter
|
||||
{
|
||||
private readonly StubDatabase _portal = new();
|
||||
private readonly StubDatabase _highRes = new();
|
||||
private readonly StubDatabase _language = new();
|
||||
private readonly StubDatabase _cell = new();
|
||||
|
||||
public string SourceDirectory => string.Empty;
|
||||
|
||||
public IDatDatabase Portal => _portal;
|
||||
|
||||
public IDatDatabase Cell => _cell;
|
||||
|
||||
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
|
||||
new(new Dictionary<uint, IDatDatabase>());
|
||||
|
||||
public IDatDatabase HighRes => _highRes;
|
||||
|
||||
public IDatDatabase Language => _language;
|
||||
|
||||
public IDatDatabase Local => _language;
|
||||
|
||||
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
|
||||
new(new Dictionary<uint, uint>());
|
||||
|
||||
public int PortalIteration => 0;
|
||||
|
||||
public int CellIteration => 0;
|
||||
|
||||
public int HighResIteration => 0;
|
||||
|
||||
public int LanguageIteration => 0;
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint regionId,
|
||||
uint fileId,
|
||||
ref byte[] bytes,
|
||||
out int bytesRead)
|
||||
{
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||
Array.Empty<uint>();
|
||||
|
||||
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
|
||||
Array.Empty<IDatReaderWriter.IdResolution>();
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public bool TrySave<T>(
|
||||
uint regionId,
|
||||
T obj,
|
||||
int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
[return: MaybeNull]
|
||||
public T Get<T>(uint fileId) where T : IDBObj => default;
|
||||
|
||||
public bool TryGet<T>(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class StubDatabase : IDatDatabase
|
||||
{
|
||||
public DatDatabase Db => throw new NotSupportedException();
|
||||
|
||||
public int Iteration => 0;
|
||||
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : IDBObj =>
|
||||
Array.Empty<uint>();
|
||||
|
||||
public bool TryGet<T>(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out byte[] value)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint fileId,
|
||||
ref byte[] bytes,
|
||||
out int bytesRead)
|
||||
{
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue