Add paired depth-write variants for all five blended SetSurface families in ordinary and atmospheric Wb pipeline sets at both sample counts and in EnvCell. Select them only from the carried AlphaTestEnabled state, retain pure-Clip and Translucent override behavior, and cover disposal plus partial-construction rollback. Correct the three stale depth oracles and the AP register count/temporary AP-232 overclaim. Required restored mutations and first discriminating failures: 1. Wb StraightAlpha+Clip collapsed to depth-off: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState failed at line 1278; expected wb-mesh-alpha-depth-write-1x, binds were wb-mesh-alpha-1x. 2. EnvCell raw Additive+Clip collapsed to depth-off: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity failed at line 132; expected envcell-raw-additive-depth-write, actual envcell-raw-additive. 3. EnvCell non-Clip raw Additive forced depth-on: the same production transcript failed at line 132; first row expected envcell-raw-additive, actual envcell-raw-additive-depth-write. 4. Late Translucent|Clip override forced alpha-test/depth-on: the same production transcript failed at line 132; expected envcell-alpha, actual envcell-alpha-depth-write.
1022 lines
41 KiB
C#
1022 lines
41 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Numerics;
|
|
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 ──────────────────────────────────────────────────────
|
|
|
|
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,
|
|
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();
|
|
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 : IDirectionalShadowReceiverSource
|
|
{
|
|
public DirectionalShadowPipelineShaders PipelineShaders =>
|
|
DirectionalShadowPipelineShaders.Local;
|
|
|
|
public bool TryGetCurrentFrameBinding(
|
|
IGpuFrame frame,
|
|
out DirectionalShadowFrameBinding binding)
|
|
{
|
|
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()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|