using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.InteropServices; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Walk; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using AcDream.Core.Meshing; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using Microsoft.Extensions.Logging.Abstractions; namespace AcDream.App.Tests.Rendering.Walk; /// /// Campaign FW stage FW2: /// (pure CPU merge-run legality) and, from Campaign FW stage FW3.4a, /// + /// (the same legality proven /// through actual recorded RHI calls against , /// replacing the single SubmitOrderedStream call those two now split). /// public sealed class OrderPreservingSubmitterTests { private static OrderedDrawCommand MakeCommand( int index, WalkDrawStage stage = WalkDrawStage.Terrain, TranslucencyKind translucency = TranslucencyKind.Opaque, CullMode cullMode = CullMode.CounterClockwise, uint detailCategory = 0, RetailSetSurfaceMaterialState? materialState = null) => new( Key: new GroupKey( FirstIndex: (uint)index * 3, BaseVertex: index * 4, IndexCount: 3, TextureSlot: new GpuTextureSlot((uint)index), TextureLayer: 0, Translucency: translucency, MaterialState: materialState ?? RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: cullMode), Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3), Stage: stage, CellId: 0x8C040100u + (uint)index, ClipSlot: 0, Lights: WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 0, Alpha: 1f, SelectionLighting: Vector2.Zero, DetailCategory: detailCategory); private static OrderedDrawStream StreamOf(params OrderedDrawCommand[] commands) { var stream = new OrderedDrawStream(); foreach (OrderedDrawCommand command in commands) stream.Append(command); return stream; } // ── Pure BuildOrderedMergeRuns — no GPU device ───────────────────────── [Fact] public void BuildOrderedMergeRuns_MergesThreeAdjacentSameStateCommandsIntoOneRun() { OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1), MakeCommand(2)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); WbDrawDispatcher.OrderedMergeRun run = Assert.Single(runs); Assert.Equal(0, run.FirstCommand); Assert.Equal(3, run.CommandCount); } [Fact] public void BuildOrderedMergeRuns_SplitsOnAPipelineBucketChange() { OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.Opaque), MakeCommand(2, translucency: TranslucencyKind.AlphaBlend)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 2), new WbDrawDispatcher.OrderedMergeRun(2, 1), ], runs); } [Fact] public void BuildOrderedMergeRuns_SplitsOnACullModeChange() { OrderedDrawStream stream = StreamOf( MakeCommand(0, cullMode: CullMode.None), MakeCommand(1, cullMode: CullMode.None), MakeCommand(2, cullMode: CullMode.Clockwise)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 2), new WbDrawDispatcher.OrderedMergeRun(2, 1), ], runs); } /// /// The load-bearing new assertion: two commands whose material state /// (bucket, cull mode, detail category) is IDENTICAL still split into two /// runs when their differs. Nothing about the /// deferred-alpha template this submitter borrows from ever had to /// consider stage — walk order introduces it. /// [Fact] public void BuildOrderedMergeRuns_SplitsOnAStageChangeEvenWithIdenticalMaterialState() { OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.CellStatic)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 1), new WbDrawDispatcher.OrderedMergeRun(1, 1), ], runs); } [Fact] public void BuildOrderedMergeRuns_ADetailCategoryCommandIsAlwaysSolo() { OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1, detailCategory: 1), MakeCommand(2)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); Assert.Equal( [ new WbDrawDispatcher.OrderedMergeRun(0, 1), new WbDrawDispatcher.OrderedMergeRun(1, 1), new WbDrawDispatcher.OrderedMergeRun(2, 1), ], runs); } [Fact] public void BuildOrderedMergeRuns_ThrowsNotSupportedForAPortalPunchCommand() { OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.PortalPunch)); Assert.Throws( () => WbDrawDispatcher.BuildOrderedMergeRuns(stream)); } [Fact] public void BuildOrderedMergeRuns_EveryCommandBelongsToExactlyOneRunInOrderWithNoGaps() { OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.Opaque, cullMode: CullMode.None), MakeCommand(1, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.Opaque, cullMode: CullMode.None), MakeCommand(2, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.None), MakeCommand(3, stage: WalkDrawStage.Terrain, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise), MakeCommand(4, stage: WalkDrawStage.CellStatic, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise), MakeCommand(5, stage: WalkDrawStage.CellStatic, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise, detailCategory: 1), MakeCommand(6, stage: WalkDrawStage.CellStatic, translucency: TranslucencyKind.AlphaBlend, cullMode: CullMode.Clockwise)); List runs = WbDrawDispatcher.BuildOrderedMergeRuns(stream); int coveredThrough = 0; int totalCommands = 0; foreach (WbDrawDispatcher.OrderedMergeRun run in runs) { Assert.Equal(coveredThrough, run.FirstCommand); Assert.True(run.CommandCount > 0); coveredThrough = run.FirstCommand + run.CommandCount; totalCommands += run.CommandCount; } Assert.Equal(stream.Count, coveredThrough); Assert.Equal(stream.Count, totalCommands); } // ── PrepareOrderedStream + DrawOrderedRange — recorded RHI calls against // RecordingGpuDevice. Campaign FW3.4a replaced the single // SubmitOrderedStream call with this pair (prepare the whole stream once, // draw it via one or more ranges) — every test below that used to call // SubmitOrderedStream now calls Prepare once and Draw the WHOLE stream as // ONE range, which is exactly SubmitOrderedStream's old behavior; the // "several ranges" and "bind once" shapes get their own tests further // down since they have no FW2 analogue. ──────────────────────────────── private static void PrepareAndDrawWhole(WbDrawDispatcher dispatcher, DrawScope draw, OrderedDrawStream stream) { dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); if (stream.Count > 0) dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count); } [Fact] public void PrepareThenDraw_AlternatingStateCommandsRecordOneDrawEachInOrder() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.AlphaBlend), MakeCommand(2, translucency: TranslucencyKind.Opaque), MakeCommand(3, translucency: TranslucencyKind.AlphaBlend)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device); Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)], ranges); } [Fact] public void PrepareThenDraw_MergesAdjacentSameStateCommandsIntoOneMultiDrawIndirect() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1), MakeCommand(2)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device); Assert.Equal([(0, 3)], ranges); } [Fact] public void PrepareThenDraw_CullModeChangeRecordsSeparateCullCallsAndSplitsTheDraw() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, cullMode: CullMode.None), MakeCommand(1, cullMode: CullMode.None), MakeCommand(2, cullMode: CullMode.Clockwise)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); Assert.Equal([(0, 2), (2, 1)], DecodeDrawRanges(fx.Device)); List cullCalls = [.. fx.Device.Calls.OfType().Select(c => c.CullMode)]; // ApplyCullModeRhi: CullMode.None -> GpuCullMode.None, CullMode.Clockwise -> GpuCullMode.Front. Assert.Equal([GpuCullMode.None, GpuCullMode.Front], cullCalls); } [Fact] public void PrepareThenDraw_StageChangeSplitsTheDrawEvenWithIdenticalMaterialState() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.CellStatic)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device)); } [Theory] [InlineData(false, 200f / 255f)] [InlineData(true, 100f / 255f)] public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace( bool paletted, float expectedReference) { using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1, detailCategory: 1), MakeCommand( 2, translucency: TranslucencyKind.ClipMap, detailCategory: 1, materialState: RetailSetSurfaceMaterialState.Resolve( SurfaceType.Base1ClipMap, texturePresent: true, textureHasPalette: paletted)), MakeCommand(3)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device); Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)], runs.Select(run => (run.Start, run.Count)).ToList()); Assert.Equal((0u, 0f), (runs[0].Constants.TextureIndexA, runs[0].Constants.ParamA)); Assert.Equal((77u, 3.5f), (runs[1].Constants.TextureIndexA, runs[1].Constants.ParamA)); Assert.Equal((77u, 3.5f), (runs[2].Constants.TextureIndexA, runs[2].Constants.ParamA)); Assert.Equal((0u, 0f), (runs[3].Constants.TextureIndexA, runs[3].Constants.ParamA)); Assert.Equal( [ "wb-mesh-opaque-1x", "wb-mesh-opaque-a2c-1x", "wb-mesh-opaque-a2c-1x", "wb-mesh-opaque-a2c-1x", "wb-mesh-opaque-a2c-1x", ], fx.Device.Calls.OfType() .Select(call => call.PipelineName) .ToArray()); Assert.Equal(4, fx.Device.Calls.OfType().Count()); Assert.Equal(expectedReference, runs[2].Constants.ParamB); GpuPipelineDescription a2c = fx.Device.CreatedPipelines .Single(pipeline => pipeline.Description.Name == "wb-mesh-opaque-a2c-1x") .Description; Assert.Equal(GpuBlendMode.None, a2c.Blend); Assert.True(a2c.AlphaToCoverage); Assert.True(a2c.Depth.Test); Assert.True(a2c.Depth.Write); Assert.Equal(WorldDepthContract.WorldCompare, a2c.Depth.Compare); } [Fact] public void PrepareThenDraw_PureClipBuildingDetailOffRetainsAp240A2cAndNeutralDetailState() { using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: false); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand( 1, translucency: TranslucencyKind.ClipMap, detailCategory: 1, materialState: RetailSetSurfaceMaterialState.Resolve( SurfaceType.Base1ClipMap, texturePresent: true, textureHasPalette: true)), MakeCommand(2)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device); Assert.Equal([(0, 1), (1, 1), (2, 1)], runs.Select(run => (run.Start, run.Count)).ToList()); Assert.Equal(0u, runs[1].Constants.TextureIndexA); Assert.Equal(0f, runs[1].Constants.ParamA); Assert.Equal(0f, runs[1].Constants.ParamB); Assert.Equal(0, runs[1].Constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag); Assert.Contains(fx.Device.Calls.OfType(), call => call.PipelineName == "wb-mesh-opaque-a2c-1x"); } [Fact] public void AtmosphericReceiver_AdjacentOrdinaryDetailOrdinaryCommandsDoNotLeakExactState() { using var fx = new DispatcherFixture( detailAvailable: true, detailEnabled: true, atmospheric: true); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Additive), MakeCommand( 1, translucency: TranslucencyKind.Additive, detailCategory: 1, materialState: RetailSetSurfaceMaterialState.Resolve( SurfaceType.Additive | SurfaceType.Base1ClipMap, texturePresent: true, textureHasPalette: false)), MakeCommand(2, translucency: TranslucencyKind.Additive)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device); Assert.Equal([(0, 1), (1, 1), (2, 1)], runs.Select(run => (run.Start, run.Count)).ToList()); Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[0].Constants)); Assert.Equal((77u, 3.5f, 200f / 255f, RetailDetailTextureContract.NoFogRenderPassFlag), DetailState(runs[1].Constants)); Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[2].Constants)); Assert.Contains(fx.Device.Calls.OfType(), call => call.PipelineName == "wb-mesh-atmospheric-raw-additive-depth-write-1x"); Assert.Contains(fx.Device.Calls.OfType(), call => call.PipelineName == "wb-mesh-atmospheric-additive-1x"); GpuPipelineDescription selected = fx.Device.CreatedPipelines .Single(pipeline => pipeline.Description.Name == "wb-mesh-atmospheric-raw-additive-depth-write-1x") .Description; Assert.Equal("mesh_atmospheric", selected.Shaders.Name); Assert.Equal(GpuBlendMode.RawAdditive, selected.Blend); Assert.True(selected.Depth.Write); GpuPipelineDescription adjacent = fx.Device.CreatedPipelines .Single(pipeline => pipeline.Description.Name == "wb-mesh-atmospheric-additive-1x") .Description; Assert.Equal(GpuBlendMode.Additive, adjacent.Blend); Assert.False(adjacent.Depth.Write); } private static (uint Slot, float Tiling, float Reference, int NoFog) DetailState( GpuPushConstants constants) => (constants.TextureIndexA, constants.ParamA, constants.ParamB, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag); [Theory] [InlineData(false, "wb-mesh")] [InlineData(true, "wb-mesh-atmospheric")] public void SetSurfacePipelineFamilies_CarryDepthWritePairsAcrossBothSampleCountsAndDispose( bool atmospheric, string prefix) { var fx = new DispatcherFixture(atmospheric: atmospheric, sampleCount: 4); var families = new (string Name, GpuBlendMode Blend)[] { ("alpha", GpuBlendMode.StraightAlpha), ("additive", GpuBlendMode.Additive), ("raw-additive", GpuBlendMode.RawAdditive), ("inverse", GpuBlendMode.InverseAlpha), ("inverse-additive", GpuBlendMode.InverseAdditive), }; var retained = new List(); foreach ((string name, GpuBlendMode blend) in families) { foreach ((string suffix, int samples) in new[] { (string.Empty, 4), ("-1x", 1) }) { RecordingGpuPipeline depthOff = fx.Device.CreatedPipelines.Single( pipeline => pipeline.Description.Name == $"{prefix}-{name}{suffix}"); RecordingGpuPipeline depthOn = fx.Device.CreatedPipelines.Single( pipeline => pipeline.Description.Name == $"{prefix}-{name}-depth-write{suffix}"); retained.Add(depthOff); retained.Add(depthOn); Assert.Equal(blend, depthOff.Description.Blend); Assert.Equal(blend, depthOn.Description.Blend); Assert.Equal(samples, depthOff.Description.SampleCount); Assert.Equal(samples, depthOn.Description.SampleCount); Assert.True(depthOff.Description.Depth.Test); Assert.True(depthOn.Description.Depth.Test); Assert.False(depthOff.Description.Depth.Write); Assert.True(depthOn.Description.Depth.Write); Assert.Equal(WorldDepthContract.WorldCompare, depthOff.Description.Depth.Compare); Assert.Equal(WorldDepthContract.WorldCompare, depthOn.Description.Depth.Compare); } } fx.Dispose(); Assert.All(retained, static pipeline => Assert.True(pipeline.IsDisposed)); } [Fact] public void SetSurfacePipelineConstructionFailureRollsBackBothSampleCountOwners() { using var device = new RecordingGpuDevice(); device.PipelineFailure = description => description.Name == "wb-mesh-inverse-depth-write-1x" ? new InvalidOperationException("injected pipeline failure") : null; Assert.Throws(() => new DispatcherFixture(sampleCount: 4, device: device)); Assert.Equal(21, device.CreatedPipelines.Count); Assert.All(device.CreatedPipelines, static pipeline => Assert.True(pipeline.IsDisposed)); } [Fact] public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.AlphaBlend)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device); Assert.Equal(2, runs.Count); Assert.Equal(0, runs[0].Constants.RenderPass); Assert.Equal(1, runs[1].Constants.RenderPass); } [Fact] public void PrepareOrderedStream_ThrowsNotSupportedForAPortalPunchCommandBeforeAnyDraw() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.PortalPunch)); Assert.Throws( () => fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity)); Assert.Empty(fx.Device.Calls.OfType()); Assert.Empty(fx.Device.Calls.OfType()); } [Fact] public void PrepareOrderedStream_EmptyStreamRecordsNoDraws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); fx.Dispatcher.PrepareOrderedStream(draw.Frame, new OrderedDrawStream(), Matrix4x4.Identity); Assert.Empty(fx.Device.Calls.OfType()); } // ── Campaign FW3.4a — the shapes with no FW2 analogue: drawing the SAME // prepared stream as several ranges, the bind-once optimization, the // fail-loud range check, and the assert-don't-slice straddle guard. ─── /// /// The whole point of the split: drawing the SAME stream as TWO ranges /// (with the boundary between them supplied to Prepare, exactly as /// WalkFrameDriver.Replay supplies its recorded mark positions) produces /// the identical total recorded draw/cull/push-constant calls as drawing /// it as one range — the range split changes nothing about what reaches /// the GPU, only how many DrawOrderedRange calls got there. /// [Fact] public void DrawOrderedRange_AsTwoRangesAtASegmentBoundary_MatchesOneRangeOverTheWholeStream() { OrderedDrawStream stream = StreamOf( MakeCommand(0, translucency: TranslucencyKind.Opaque), MakeCommand(1, translucency: TranslucencyKind.Opaque), MakeCommand(2, translucency: TranslucencyKind.Opaque), MakeCommand(3, translucency: TranslucencyKind.Opaque)); using var wholeFx = new DispatcherFixture(); using (DrawScope draw = wholeFx.BeginDraw()) { wholeFx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); wholeFx.Dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count); } List<(int Start, int Count)> wholeRanges = DecodeDrawRanges(wholeFx.Device); using var splitFx = new DispatcherFixture(); using (DrawScope draw = splitFx.BeginDraw()) { // Command 2 is a segment boundary (mirrors a mark WalkFrameDriver // would record there, e.g. a cell shell between two same-state // segments) — without it, all four commands would merge into ONE // run; the boundary forces two. splitFx.Dispatcher.PrepareOrderedStream( draw.Frame, stream, Matrix4x4.Identity, forcedBreaksAscending: [2]); splitFx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2); splitFx.Dispatcher.DrawOrderedRange(draw.Pass, 2, 2); } List<(int Start, int Count)> splitRanges = DecodeDrawRanges(splitFx.Device); // The split path draws two runs where the whole-range path drew one // (the forced boundary is the only difference) — but every command // reaches the GPU exactly once, in order, with identical coverage. Assert.Equal([(0, 4)], wholeRanges); Assert.Equal([(0, 2), (2, 2)], splitRanges); } /// /// The FW3.4a perf shape itself: the nine per-instance storage binds plus /// the warm-up pipeline bind happen on the FIRST DrawOrderedRange call in /// a frame only — a second call over the same prepared payload issues no /// further StorageBind calls, which is the whole reason this stage exists /// (the old SubmitOrderedStream rebound everything on every call). /// [Fact] public void DrawOrderedRange_EveryCallRebindsTheStorageSections() { // The corrected FW3.4a contract (the dense-Arwic device-lost fix): // between ordered ranges the walk's leaf draws and RetailAlphaQueue // flushes rebind the SAME set-0 slots to THEIR sections, so every // DrawOrderedRange call must re-bind its own — a latched skip draws // the next range against foreign buffers. Only the ring WRITES are // once-per-frame (PrepareOrderedStream); binds repeat per range, // exactly like DrawPreparedAlphaBatchRhi. using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf( MakeCommand(0, stage: WalkDrawStage.Terrain), MakeCommand(1, stage: WalkDrawStage.CellStatic)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1); int boundAfterFirst = fx.Device.Calls.OfType().Count(); Assert.True(boundAfterFirst > 0); fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1); int boundAfterSecond = fx.Device.Calls.OfType().Count(); Assert.Equal(boundAfterFirst * 2, boundAfterSecond); // Both commands drew; the rebinds changed nothing about coverage. Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device)); } /// /// Fail-loud range check (mirrors DrawPreparedAlphaBatchRhi's): a range /// outside what PrepareOrderedStream uploaded throws rather than drawing /// garbage or silently clamping — including a draw attempted before ANY /// Prepare call this frame. /// [Fact] public void DrawOrderedRange_RangeExceedingThePreparedPayload_Throws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); OrderedDrawStream stream = StreamOf(MakeCommand(0)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2)); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1)); } [Fact] public void DrawOrderedRange_BeforeAnyPrepareCallThisFrame_Throws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1)); } /// /// The plan's "assert it" rule: a range that does not align with a merge /// run boundary throws rather than silently slicing the run — proven /// directly here (skipping the boundary a real WalkFrameDriver mark would /// supply) since production code always supplies the boundary and would /// never exercise this path. /// [Fact] public void DrawOrderedRange_RangeStraddlingAMergeRun_Throws() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); // All four commands share stage/bucket/cull — ONE merge run [0, 4) — // and no forced break is supplied, so a [0, 2) range straddles it. OrderedDrawStream stream = StreamOf( MakeCommand(0), MakeCommand(1), MakeCommand(2), MakeCommand(3)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); Assert.Throws( () => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2)); } /// /// Fail-loud invariant: whatever the state pattern, the recorded /// MultiDrawIndirect calls' DrawCounts always sum to the stream's Count — /// no command is ever silently skipped, and none is drawn twice. /// [Fact] public void PrepareThenDraw_TotalRecordedDrawCountAlwaysEqualsTheStreamCount() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); var stream = new OrderedDrawStream(); var stages = new[] { WalkDrawStage.Terrain, WalkDrawStage.CellStatic, WalkDrawStage.BuildingShell }; var blends = new[] { TranslucencyKind.Opaque, TranslucencyKind.AlphaBlend, TranslucencyKind.Additive, TranslucencyKind.InvAlpha, }; var culls = new[] { CullMode.None, CullMode.Clockwise, CullMode.CounterClockwise }; const int commandCount = 11; for (int i = 0; i < commandCount; i++) { stream.Append(MakeCommand( i, stage: stages[i % stages.Length], translucency: blends[i % blends.Length], cullMode: culls[i % culls.Length], detailCategory: i == 5 ? 1u : 0u)); } PrepareAndDrawWhole(fx.Dispatcher, draw, stream); List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device); int sum = ranges.Sum(r => r.Count); Assert.Equal(commandCount, sum); int coveredThrough = 0; foreach ((int start, int count) in ranges) { Assert.Equal(coveredThrough, start); coveredThrough += count; } Assert.Equal(commandCount, coveredThrough); } // ── Decode helpers ────────────────────────────────────────────────────── [Theory] [InlineData(false, "mesh_modern")] [InlineData(true, "mesh_atmospheric")] public void SharedTransformPrefix_RecordsAbsoluteCommandsAndZeroBasedLocalSidecars( bool receiverBindingAvailable, string expectedShader) { using var fx = new DispatcherFixture( atmospheric: true, receiverBindingAvailable: receiverBindingAvailable); using DrawScope draw = fx.BeginDraw(); Matrix4x4[] shadowPrefix = [ Matrix4x4.CreateTranslation(101f, 102f, 103f), Matrix4x4.CreateTranslation(201f, 202f, 203f), Matrix4x4.CreateTranslation(301f, 302f, 303f), ]; WorldTransformFrameSlice shared = fx.Dispatcher.BeginDirectionalShadowTransformFrame(draw.Frame, shadowPrefix); Assert.Equal(3u, shared.InstanceCount); OrderedDrawCommand first = MakeCommand(0) with { Transform = Matrix4x4.CreateTranslation(-0.5f, 1.25f, 2.5f), ClipSlot = 11u, Lights = new WbDrawDispatcher.InstanceLightSet(0, 2, 4, 6, -1, -1, -1, -1), IndoorFlag = 0u, Alpha = 0.25f, SelectionLighting = new Vector2(0.125f, 0.375f), DetailCategory = 7u, }; OrderedDrawCommand second = MakeCommand(1) with { Transform = Matrix4x4.CreateTranslation(0.75f, -1.5f, 3.25f), ClipSlot = 22u, Lights = new WbDrawDispatcher.InstanceLightSet(1, 3, 5, 7, -1, -1, -1, -1), IndoorFlag = 1u, Alpha = 0.75f, SelectionLighting = new Vector2(0.625f, 0.875f), DetailCategory = 9u, }; OrderedDrawStream stream = StreamOf(first, second); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); Assert.Contains( fx.Device.Calls.OfType(), call => fx.Device.CreatedPipelines.Single( pipeline => pipeline.Description.Name == call.PipelineName) .Description.Shaders.Name == expectedShader); GpuRecordedMultiDrawIndirect[] drawCalls = fx.Device.Calls.OfType().ToArray(); Assert.Equal(2, drawCalls.Length); Assert.All(drawCalls, call => Assert.Equal(1u, call.DrawCount)); Assert.Equal(drawCalls[0].OffsetBytes + drawCalls[0].StrideBytes, drawCalls[1].OffsetBytes); GpuRecordedMultiDrawIndirect drawCall = drawCalls[0]; ReadOnlySpan commands = MemoryMarshal.Cast( fx.Device.RingBytes.Slice((int)drawCall.OffsetBytes, checked((int)drawCall.StrideBytes * 2))); Assert.Equal([3u, 4u], commands.ToArray().Select(command => command.BaseInstance).ToArray()); GpuRecordedPushConstants pushed = fx.Device.Calls .TakeWhile(call => !ReferenceEquals(call, drawCall)) .OfType() .Last(); Assert.Equal(3u, pushed.Constants.TextureIndexB); AssertLocalSection(GpuBindingModel.StorageClipSlots, [11u, 22u]); AssertLocalSection( GpuBindingModel.StorageInstanceLightSets, [0, 2, 4, 6, -1, -1, -1, -1, 1, 3, 5, 7, -1, -1, -1, -1]); AssertLocalSection(GpuBindingModel.StorageInstanceIndoor, [0u, 1u]); AssertLocalSection(GpuBindingModel.StorageInstanceAlpha, [0.25f, 0.75f]); AssertLocalSection( GpuBindingModel.StorageInstanceSelectionLighting, [new Vector2(0.125f, 0.375f), new Vector2(0.625f, 0.875f)]); AssertLocalSection(GpuBindingModel.StorageInstanceDetailCategory, [7u, 9u]); Assert.Equal( 6, new uint[] { 3, 5, 6, 7, 8, 9 } .Select(binding => LastBind(binding).OffsetBytes) .Distinct() .Count()); GpuRecordedStorageBind transformBind = LastBind(GpuBindingModel.StorageInstances); Assert.Equal(shared.BaseOffsetBytes, transformBind.OffsetBytes); ReadOnlySpan transforms = MemoryMarshal.Cast( fx.Device.RingBytes.Slice( (int)transformBind.OffsetBytes, checked((int)(5u * WorldTransformCapacityPolicy.MatrixBytes)))); Assert.Equal(first.Transform, transforms[3]); Assert.Equal(second.Transform, transforms[4]); void AssertLocalSection(uint binding, T[] expected) where T : unmanaged { GpuRecordedStorageBind bound = LastBind(binding); int byteCount = checked(expected.Length * Marshal.SizeOf()); Assert.Equal((uint)byteCount, bound.SizeBytes); Assert.Equal( expected, MemoryMarshal.Cast( fx.Device.RingBytes.Slice((int)bound.OffsetBytes, byteCount)).ToArray()); } GpuRecordedStorageBind LastBind(uint binding) => fx.Device.Calls .TakeWhile(call => !ReferenceEquals(call, drawCall)) .OfType() .Last(call => call.Binding == binding); } private static List<(int Start, int Count)> DecodeDrawRanges(RecordingGpuDevice device) => [.. DecodeRuns(device).Select(r => (r.Start, r.Count))]; private static List<(GpuPushConstants Constants, int Start, int Count)> DecodeRuns( RecordingGpuDevice device) { GpuPushConstants? lastConstants = null; uint? commandBase = null; var result = new List<(GpuPushConstants, int, int)>(); foreach (var call in device.Calls) { if (call is GpuRecordedPushConstants pc) { lastConstants = pc.Constants; } else if (call is GpuRecordedMultiDrawIndirect mdi) { Assert.Equal((uint)WbDrawDispatcher.DrawCommandStride, mdi.StrideBytes); commandBase ??= mdi.OffsetBytes; int start = (int)((mdi.OffsetBytes - commandBase.Value) / mdi.StrideBytes); Assert.NotNull(lastConstants); result.Add((lastConstants!.Value, start, (int)mdi.DrawCount)); } } return result; } // ── Fixture: a real WbDrawDispatcher against RecordingGpuDevice ───────── 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 detailAvailable = false, bool detailEnabled = false, bool atmospheric = false, bool receiverBindingAvailable = true, int sampleCount = 1, RecordingGpuDevice? device = null) { Device = device ?? new RecordingGpuDevice(); FrameLifetime = new GpuDeviceFrameLifetime(Device); Scope = new VulkanWorldPassScope(sampleCount); _textures = new TextureCache(Device, new NoopDatReaderWriter()); _meshAdapter = new WbMeshAdapter( Device, new NoopDatReaderWriter(), new NullPreparedAssetSource(), NullLogger.Instance, Device.Retirement); var entitySpawnAdapter = new EntitySpawnAdapter( _textures, _ => throw new NotSupportedException( "Not exercised by SubmitOrderedStream tests.")); Dispatcher = new WbDrawDispatcher( Device, FrameLifetime, Scope, _textures, _meshAdapter, entitySpawnAdapter, new EntityClassificationCache(), new AcDream.Core.Rendering.TranslucencyFadeManager(), buildingDetail: detailAvailable ? new TerrainAtlas.RetailDetailTextureBinding( new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16) : default, buildingDetailEnabled: () => detailEnabled); if (atmospheric) { var source = new BindableAtmosphericSource(receiverBindingAvailable); WbDrawDispatcher.DirectionalShadowReceiverPipelineState candidate = Assert.IsType( Dispatcher.PrepareDirectionalShadowReceiver(source, sampleCount)); Assert.Null(Dispatcher.SwapDirectionalShadowReceiver(candidate)); } Atmospheric = atmospheric; } public RecordingGpuDevice Device { get; } public GpuDeviceFrameLifetime FrameLifetime { get; } public VulkanWorldPassScope Scope { get; } public WbDrawDispatcher Dispatcher { get; } public bool Atmospheric { get; } /// Opens a frame and a backbuffer pass, publishes it on /// , then clears the recorded calls so a test only /// sees what its own SubmitOrderedStream call produced. public DrawScope BeginDraw() { FrameLifetime.BeginFrame(); IGpuFrame frame = FrameLifetime.CurrentFrame!; IGpuPassEncoder pass = frame.BeginPass( GpuPassDescription.BackbufferClear( Atmospheric ? DirectionalShadowReceiverPolicy.AtmosphericWorldPassName : "fw2-ordered-stream-test", Vector4.Zero, Scope.SampleCount)); 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 BindableAtmosphericSource(bool bindingAvailable) : IDirectionalShadowReceiverSource { public DirectionalShadowPipelineShaders PipelineShaders => DirectionalShadowPipelineShaders.Local; public bool TryGetCurrentFrameBinding( IGpuFrame frame, out DirectionalShadowFrameBinding binding) { if (!bindingAvailable) { binding = DirectionalShadowFrameBinding.Disabled; return false; } GpuRingAllocation allocation = frame.AllocateRing( checked((int)DirectionalShadowUniforms.SizeInBytes), GpuRingUsage.Uniform); binding = new DirectionalShadowFrameBinding( frame.Serial, Enabled: false, allocation.Buffer, allocation.OffsetBytes, DirectionalShadowUniforms.SizeInBytes, GpuTextureSlot.Unassigned, CascadeCount: 0); return true; } } 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 CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => _highRes; public IDatDatabase Language => _language; public IDatDatabase Local => _language; public ReadOnlyDictionary RegionFileMap { get; } = new(new Dictionary()); 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 GetAllIdsOfType() where T : IDBObj => Array.Empty(); public IEnumerable ResolveId(uint id) => Array.Empty(); public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public bool TrySave( uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => default; public bool TryGet( 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 GetAllIdsOfType() where T : IDBObj => Array.Empty(); public bool TryGet( 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 obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public void Dispose() { } } } }