Use the shared absolute base-instance domain only for mesh transforms and subtract the published transform prefix for every live ordinary sidecar. Regenerate the production module and pin its hash. Add real ordered-recording, committed-SPIR-V structure, and headless production-Vulkan pixel witnesses at nonzero prefixes, including receiver active/inactive invariance. Mutations performed and restored: 1. Transform lookup -> local instanceIndex: MeshModernSharedIndexSpirvTests first failed Assert.Contains, item 368 not found in [27,377,27] (and the pixel witness found 0 dark pixels). 2. Selection sidecar -> absolute transformIndex: MeshModernSharedIndexOffscreenTests first failed: Expected a dark local-sidecar instance, found 0 matching pixels. 3. Published TextureIndexB -> 0: SharedTransformPrefix recording first failed Assert.Equal, expected 3, actual 0. 4. Receiver choice inverted: inactive first failed because expected mesh_modern was absent and only mesh_atmospheric was recorded; active failed conversely. 5. Offscreen shader directory -> copied test output: witness first failed the exact-path Assert.Equal (expected repo src/AcDream.App/Rendering/Shaders/spv, actual tests/AcDream.App.Tests/bin/Release/net10.0/Rendering/Shaders/spv).
1140 lines
46 KiB
C#
1140 lines
46 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Campaign FW stage FW2: <see cref="WbDrawDispatcher.BuildOrderedMergeRuns"/>
|
|
/// (pure CPU merge-run legality) and, from Campaign FW stage FW3.4a,
|
|
/// <see cref="WbDrawDispatcher.PrepareOrderedStream"/> +
|
|
/// <see cref="WbDrawDispatcher.DrawOrderedRange"/> (the same legality proven
|
|
/// through actual recorded RHI calls against <see cref="RecordingGpuDevice"/>,
|
|
/// replacing the single <c>SubmitOrderedStream</c> call those two now split).
|
|
/// </summary>
|
|
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<WbDrawDispatcher.OrderedMergeRun> 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<WbDrawDispatcher.OrderedMergeRun> 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<WbDrawDispatcher.OrderedMergeRun> runs =
|
|
WbDrawDispatcher.BuildOrderedMergeRuns(stream);
|
|
|
|
Assert.Equal(
|
|
[
|
|
new WbDrawDispatcher.OrderedMergeRun(0, 2),
|
|
new WbDrawDispatcher.OrderedMergeRun(2, 1),
|
|
],
|
|
runs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The load-bearing new assertion: two commands whose material state
|
|
/// (bucket, cull mode, detail category) is IDENTICAL still split into two
|
|
/// runs when their <see cref="WalkDrawStage"/> differs. Nothing about the
|
|
/// deferred-alpha template this submitter borrows from ever had to
|
|
/// consider stage — walk order introduces it.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildOrderedMergeRuns_SplitsOnAStageChangeEvenWithIdenticalMaterialState()
|
|
{
|
|
OrderedDrawStream stream = StreamOf(
|
|
MakeCommand(0, stage: WalkDrawStage.Terrain),
|
|
MakeCommand(1, stage: WalkDrawStage.CellStatic));
|
|
|
|
List<WbDrawDispatcher.OrderedMergeRun> 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<WbDrawDispatcher.OrderedMergeRun> 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<NotSupportedException>(
|
|
() => 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<WbDrawDispatcher.OrderedMergeRun> 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<GpuCullMode> cullCalls =
|
|
[.. fx.Device.Calls.OfType<GpuRecordedCullMode>().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<GpuRecordedPipelineBind>()
|
|
.Select(call => call.PipelineName)
|
|
.ToArray());
|
|
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().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<GpuRecordedPipelineBind>(),
|
|
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<GpuRecordedPipelineBind>(),
|
|
call => call.PipelineName == "wb-mesh-atmospheric-raw-additive-depth-write-1x");
|
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
|
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<RecordingGpuPipeline>();
|
|
|
|
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<InvalidOperationException>(() =>
|
|
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<NotSupportedException>(
|
|
() => fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity));
|
|
|
|
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
|
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedStorageBind>());
|
|
}
|
|
|
|
[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<GpuRecordedMultiDrawIndirect>());
|
|
}
|
|
|
|
// ── 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. ───
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
[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<GpuRecordedStorageBind>().Count();
|
|
Assert.True(boundAfterFirst > 0);
|
|
|
|
fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1);
|
|
int boundAfterSecond = fx.Device.Calls.OfType<GpuRecordedStorageBind>().Count();
|
|
|
|
Assert.Equal(boundAfterFirst * 2, boundAfterSecond);
|
|
// Both commands drew; the rebinds changed nothing about coverage.
|
|
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<ArgumentOutOfRangeException>(
|
|
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2));
|
|
Assert.Throws<ArgumentOutOfRangeException>(
|
|
() => 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<ArgumentOutOfRangeException>(
|
|
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<InvalidOperationException>(
|
|
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<GpuRecordedPipelineBind>(),
|
|
call => fx.Device.CreatedPipelines.Single(
|
|
pipeline => pipeline.Description.Name == call.PipelineName)
|
|
.Description.Shaders.Name == expectedShader);
|
|
|
|
GpuRecordedMultiDrawIndirect[] drawCalls =
|
|
fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().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<DrawElementsIndirectCommand> commands = MemoryMarshal.Cast<byte, DrawElementsIndirectCommand>(
|
|
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<GpuRecordedPushConstants>()
|
|
.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<Matrix4x4> transforms = MemoryMarshal.Cast<byte, Matrix4x4>(
|
|
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<T>(uint binding, T[] expected) where T : unmanaged
|
|
{
|
|
GpuRecordedStorageBind bound = LastBind(binding);
|
|
int byteCount = checked(expected.Length * Marshal.SizeOf<T>());
|
|
Assert.Equal((uint)byteCount, bound.SizeBytes);
|
|
Assert.Equal(
|
|
expected,
|
|
MemoryMarshal.Cast<byte, T>(
|
|
fx.Device.RingBytes.Slice((int)bound.OffsetBytes, byteCount)).ToArray());
|
|
}
|
|
|
|
GpuRecordedStorageBind LastBind(uint binding) => fx.Device.Calls
|
|
.TakeWhile(call => !ReferenceEquals(call, drawCall))
|
|
.OfType<GpuRecordedStorageBind>()
|
|
.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<WbMeshAdapter>.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<WbDrawDispatcher.DirectionalShadowReceiverPipelineState>(
|
|
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; }
|
|
|
|
/// <summary>Opens a frame and a backbuffer pass, publishes it on
|
|
/// <see cref="Scope"/>, then clears the recorded calls so a test only
|
|
/// sees what its own <c>SubmitOrderedStream</c> call produced.</summary>
|
|
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<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()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|