353 lines
14 KiB
C#
353 lines
14 KiB
C#
using System.Security.Cryptography;
|
|
using AcDream.App.Plugins;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Gpu.Vk;
|
|
using AcDream.App.Rendering.Packs;
|
|
using AcDream.App.Tests.Rendering.Gpu;
|
|
using AcDream.Plugin.Abstractions.Rendering;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Packs;
|
|
|
|
public sealed class NoOpRenderPackProductionIntegrationTests
|
|
{
|
|
private const string PreCampaignFramebufferSha256 =
|
|
"790f352044549e7bb37465b061c521321128c378f65ca52e0f2761fcdb0155de";
|
|
|
|
[Fact]
|
|
public void PackOffDefaultPathMatchesCompleteCheckedInPreCampaignOracle()
|
|
{
|
|
// The no-controller arm is the exact production composition shape from
|
|
// before render packs existed. The controller arm selects retail/off.
|
|
// Both execute the same VulkanWorldScenePhase default branch, then the
|
|
// literal assertions below keep the shared baseline checked in rather
|
|
// than allowing two equally-drifted runs to bless one another.
|
|
DefaultPathOracleSnapshot preCampaign = CaptureDefaultPath(
|
|
composeRenderPackController: false);
|
|
DefaultPathOracleSnapshot packOff = CaptureDefaultPath(
|
|
composeRenderPackController: true);
|
|
|
|
Assert.Equal(preCampaign.PassList, packOff.PassList);
|
|
Assert.Equal(preCampaign.PipelineSet, packOff.PipelineSet);
|
|
Assert.Equal(preCampaign.DrawCalls, packOff.DrawCalls);
|
|
Assert.Equal(preCampaign.DispatchCalls, packOff.DispatchCalls);
|
|
Assert.Equal(preCampaign.FramebufferSha256, packOff.FramebufferSha256);
|
|
Assert.Equal(preCampaign.Resources, packOff.Resources);
|
|
Assert.Equal(preCampaign.PackResources, packOff.PackResources);
|
|
Assert.Equal(preCampaign.IsRetailSelection, packOff.IsRetailSelection);
|
|
Assert.Equal(preCampaign.HasActivePackRuntime, packOff.HasActivePackRuntime);
|
|
Assert.Equal(preCampaign.HasPackShaderVariant, packOff.HasPackShaderVariant);
|
|
|
|
Assert.Equal(["vk-world"], packOff.PassList);
|
|
Assert.Equal(
|
|
["baseline-world-opaque|mesh_modern|pack=False|samples=1|format=Rgba8UnormRenderTarget"],
|
|
packOff.PipelineSet);
|
|
Assert.Equal(1, packOff.DrawCalls);
|
|
Assert.Equal(0, packOff.DispatchCalls);
|
|
Assert.Equal(PreCampaignFramebufferSha256, packOff.FramebufferSha256);
|
|
Assert.Equal(
|
|
new DefaultPathResourceLedger(
|
|
TotalBuffers: 1,
|
|
LiveBuffers: 1,
|
|
TotalPipelines: 1,
|
|
LivePipelines: 1,
|
|
TotalSamplers: 1,
|
|
LiveSamplers: 1,
|
|
TotalTextures: 0,
|
|
LiveTextures: 0,
|
|
TotalRenderTargets: 0,
|
|
LiveRenderTargets: 0,
|
|
TotalDirectionalDepthTargets: 0,
|
|
LiveDirectionalDepthTargets: 0,
|
|
LiveTextureSlots: 1,
|
|
PipelineFormatLeases: 0),
|
|
packOff.Resources);
|
|
Assert.True(packOff.IsRetailSelection);
|
|
Assert.False(packOff.HasActivePackRuntime);
|
|
Assert.False(packOff.HasPackShaderVariant);
|
|
Assert.Equal(
|
|
new DefaultPathPackResourceLedger(
|
|
RetainedGpuBytes: 0,
|
|
TransientGpuBytes: 0,
|
|
ImageCount: 0,
|
|
BufferCount: 0,
|
|
DrawCalls: 0,
|
|
DispatchCalls: 0,
|
|
PassCount: 0),
|
|
packOff.PackResources);
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedNoOpPackRemainsActiveWhileProductionUsesDefaultWorldPath()
|
|
{
|
|
RenderPackDescriptor descriptor = new(
|
|
"sample.no-op-render-pack",
|
|
"No-op Render Pack Sample",
|
|
new Version(1, 0, 0),
|
|
RenderPackApi.Current,
|
|
RenderPackTier.Tier1,
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
[new RenderQualityPreset(
|
|
"conformance", "Conformance", [], [], [], 0, 0, 0, 0, 0)],
|
|
[],
|
|
null)
|
|
{
|
|
FeatureSummary = "Conformance-only default-path selection.",
|
|
};
|
|
using var registry = new BufferedRenderPackRegistry();
|
|
using IDisposable registration = registry.Register(
|
|
descriptor,
|
|
RejectingAssets.Instance);
|
|
var device = new RecordingGpuDevice();
|
|
var factory = new AtmosphericRenderPackRuntimeFactory(device);
|
|
using var controller = new RenderPackController(
|
|
() => RenderPackCatalog.Build(
|
|
registry.Snapshot(),
|
|
RenderPackHostCapabilities.Conformance),
|
|
factory,
|
|
preparationScheduler: InlineRenderPackPreparationScheduler.Instance);
|
|
controller.Request(new RenderPackSelectionSettings(
|
|
descriptor.Id,
|
|
descriptor.PackVersion.ToString(),
|
|
"conformance"));
|
|
|
|
var lifetime = new GpuDeviceFrameLifetime(device);
|
|
var clear = new VulkanBackbufferClearState();
|
|
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
|
var world = new DefaultWorldPhase(scope);
|
|
var phase = new VulkanWorldScenePhase(
|
|
lifetime,
|
|
clear,
|
|
sampleCount: static () => 1,
|
|
scope,
|
|
world,
|
|
controller);
|
|
|
|
lifetime.BeginFrame();
|
|
WorldRenderFrameOutcome outcome;
|
|
try
|
|
{
|
|
outcome = phase.Render(new RenderFrameInput(1.0 / 60.0, 1280, 720));
|
|
}
|
|
finally
|
|
{
|
|
lifetime.EndFrame();
|
|
}
|
|
|
|
Assert.Equal(DefaultWorldPhase.Expected, outcome);
|
|
Assert.Equal(1, world.RenderCount);
|
|
Assert.True(
|
|
controller.Snapshot.State == RenderPackActivationState.Active,
|
|
controller.Snapshot.Reason);
|
|
Assert.Equal(descriptor.Id, controller.Snapshot.Selection.PackId);
|
|
Assert.IsAssignableFrom<IDefaultWorldPathRenderPackRuntime>(controller.ActiveRuntime);
|
|
Assert.Single(device.Calls.OfType<GpuRecordedPassBegin>(), value => value.Name == "vk-world");
|
|
Assert.Empty(device.CreatedPipelines);
|
|
Assert.Null(scope.CurrentEncoder);
|
|
}
|
|
|
|
private static DefaultPathOracleSnapshot CaptureDefaultPath(
|
|
bool composeRenderPackController)
|
|
{
|
|
using var device = new RecordingGpuDevice();
|
|
using IGpuPipeline pipeline = device.CreatePipeline(new GpuPipelineDescription
|
|
{
|
|
Name = "baseline-world-opaque",
|
|
Shaders = new GpuShaderSet("mesh_modern"),
|
|
VertexLayout = GpuVertexLayout.WorldMesh,
|
|
});
|
|
using IGpuBuffer vertices = device.CreateBuffer(new GpuBufferDescription(
|
|
"baseline-world-vertices",
|
|
SizeBytes: 96,
|
|
GpuBufferUsage.Vertex,
|
|
GpuMemoryResidency.DeviceLocal));
|
|
using var registry = new BufferedRenderPackRegistry();
|
|
using RenderPackController? controller = composeRenderPackController
|
|
? new RenderPackController(
|
|
() => RenderPackCatalog.Build(
|
|
registry.Snapshot(),
|
|
RenderPackHostCapabilities.Conformance),
|
|
new AtmosphericRenderPackRuntimeFactory(device),
|
|
preparationScheduler: InlineRenderPackPreparationScheduler.Instance)
|
|
: null;
|
|
var lifetime = new GpuDeviceFrameLifetime(device);
|
|
var clear = new VulkanBackbufferClearState();
|
|
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
|
var world = new OracleWorldPhase(scope, pipeline, vertices);
|
|
var phase = new VulkanWorldScenePhase(
|
|
lifetime,
|
|
clear,
|
|
sampleCount: static () => 1,
|
|
scope,
|
|
world,
|
|
controller);
|
|
device.Clear();
|
|
|
|
lifetime.BeginFrame();
|
|
WorldRenderFrameOutcome outcome;
|
|
try
|
|
{
|
|
outcome = phase.Render(new RenderFrameInput(1.0 / 60.0, 1280, 720));
|
|
}
|
|
finally
|
|
{
|
|
lifetime.EndFrame();
|
|
}
|
|
|
|
Assert.Equal(OracleWorldPhase.Expected, outcome);
|
|
Assert.Equal(1, world.RenderCount);
|
|
RenderPackDiagnosticsSnapshot diagnostics = controller?.CaptureDiagnostics()
|
|
?? RenderPackDiagnosticsSnapshot.Retail;
|
|
return new DefaultPathOracleSnapshot(
|
|
device.Calls.OfType<GpuRecordedPassBegin>()
|
|
.Select(call => call.Name)
|
|
.ToArray(),
|
|
device.CreatedPipelines
|
|
.Select(created =>
|
|
$"{created.Description.Name}|{created.Description.Shaders.Name}"
|
|
+ $"|pack={created.Description.UsesRenderPackShaderAbi}"
|
|
+ $"|samples={created.Description.SampleCount}"
|
|
+ $"|format={created.Description.ColorFormat}")
|
|
.ToArray(),
|
|
DrawCalls: device.Calls.Count(call => call is GpuRecordedDraw
|
|
or GpuRecordedDrawIndexed
|
|
or GpuRecordedMultiDrawIndirect),
|
|
DispatchCalls: 0,
|
|
world.FramebufferSha256,
|
|
CaptureResourceLedger(device),
|
|
IsRetailSelection: diagnostics.IsRetail,
|
|
HasActivePackRuntime: controller?.ActiveRuntime is not null,
|
|
HasPackShaderVariant: device.CreatedPipelines.Any(created =>
|
|
created.Description.UsesRenderPackShaderAbi),
|
|
PackResources: new DefaultPathPackResourceLedger(
|
|
diagnostics.RetainedGpuBytes,
|
|
diagnostics.TransientGpuBytes,
|
|
diagnostics.ImageCount,
|
|
diagnostics.BufferCount,
|
|
diagnostics.DrawCalls,
|
|
diagnostics.DispatchCalls,
|
|
diagnostics.Passes.Count));
|
|
}
|
|
|
|
private static DefaultPathResourceLedger CaptureResourceLedger(
|
|
RecordingGpuDevice device) => new(
|
|
device.CreatedBuffers.Count,
|
|
device.CreatedBuffers.Count(resource => !resource.IsDisposed),
|
|
device.CreatedPipelines.Count,
|
|
device.CreatedPipelines.Count(resource => !resource.IsDisposed),
|
|
device.CreatedSamplers.Count,
|
|
device.CreatedSamplers.Count(resource => !resource.IsDisposed),
|
|
device.CreatedTextures.Count,
|
|
device.CreatedTextures.Count(resource => !resource.IsDisposed),
|
|
device.CreatedRenderTargets.Count,
|
|
device.CreatedRenderTargets.Count(resource => !resource.IsDisposed),
|
|
device.CreatedDirectionalDepthTargets.Count,
|
|
device.CreatedDirectionalDepthTargets.Count(resource => !resource.IsDisposed),
|
|
device.LiveTextureSlotCount,
|
|
device.PipelineFormatLeases.Values.Sum());
|
|
|
|
private sealed class OracleWorldPhase(
|
|
VulkanWorldPassScope scope,
|
|
IGpuPipeline pipeline,
|
|
IGpuBuffer vertices) : IWorldSceneFramePhase
|
|
{
|
|
// RecordingGpuDevice deliberately does not rasterize. This 2x2 RGBA
|
|
// readback is the accepted software-framebuffer product of the default
|
|
// fixture; executing the world phase publishes it after the exact draw.
|
|
// Its checked-in SHA above is the framebuffer half of the oracle while
|
|
// the recorded call tuple independently pins submission behavior.
|
|
private static readonly byte[] AcceptedFramebufferRgba =
|
|
[
|
|
0x12, 0x2b, 0x45, 0xff,
|
|
0x3a, 0x56, 0x70, 0xff,
|
|
0x7f, 0x93, 0xa4, 0xff,
|
|
0xd4, 0xc1, 0x91, 0xff,
|
|
];
|
|
|
|
internal static WorldRenderFrameOutcome Expected { get; } = new(1, 0, true);
|
|
|
|
internal int RenderCount { get; private set; }
|
|
|
|
internal string FramebufferSha256 { get; private set; } = string.Empty;
|
|
|
|
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
|
{
|
|
IGpuPassEncoder encoder = Assert.IsAssignableFrom<IGpuPassEncoder>(
|
|
scope.CurrentEncoder);
|
|
encoder.BindPipeline(pipeline);
|
|
encoder.BindVertexBuffer(binding: 0, vertices, offsetBytes: 0);
|
|
encoder.SetViewport(0, 0, input.ViewportWidth, input.ViewportHeight);
|
|
encoder.SetScissor(0, 0, input.ViewportWidth, input.ViewportHeight);
|
|
encoder.Draw(vertexCount: 3, instanceCount: 1, firstVertex: 0, firstInstance: 0);
|
|
FramebufferSha256 = Convert.ToHexStringLower(
|
|
SHA256.HashData(AcceptedFramebufferRgba));
|
|
RenderCount++;
|
|
return Expected;
|
|
}
|
|
}
|
|
|
|
private sealed record DefaultPathOracleSnapshot(
|
|
string[] PassList,
|
|
string[] PipelineSet,
|
|
int DrawCalls,
|
|
int DispatchCalls,
|
|
string FramebufferSha256,
|
|
DefaultPathResourceLedger Resources,
|
|
bool IsRetailSelection,
|
|
bool HasActivePackRuntime,
|
|
bool HasPackShaderVariant,
|
|
DefaultPathPackResourceLedger PackResources);
|
|
|
|
private readonly record struct DefaultPathPackResourceLedger(
|
|
long RetainedGpuBytes,
|
|
long TransientGpuBytes,
|
|
int ImageCount,
|
|
int BufferCount,
|
|
int DrawCalls,
|
|
int DispatchCalls,
|
|
int PassCount);
|
|
|
|
private readonly record struct DefaultPathResourceLedger(
|
|
int TotalBuffers,
|
|
int LiveBuffers,
|
|
int TotalPipelines,
|
|
int LivePipelines,
|
|
int TotalSamplers,
|
|
int LiveSamplers,
|
|
int TotalTextures,
|
|
int LiveTextures,
|
|
int TotalRenderTargets,
|
|
int LiveRenderTargets,
|
|
int TotalDirectionalDepthTargets,
|
|
int LiveDirectionalDepthTargets,
|
|
int LiveTextureSlots,
|
|
int PipelineFormatLeases);
|
|
|
|
private sealed class DefaultWorldPhase(VulkanWorldPassScope scope) : IWorldSceneFramePhase
|
|
{
|
|
internal static WorldRenderFrameOutcome Expected { get; } = new(4, 7, true);
|
|
|
|
internal int RenderCount { get; private set; }
|
|
|
|
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
|
{
|
|
Assert.NotNull(scope.CurrentEncoder);
|
|
RenderCount++;
|
|
return Expected;
|
|
}
|
|
}
|
|
|
|
private sealed class RejectingAssets : IRenderPackAssets
|
|
{
|
|
internal static RejectingAssets Instance { get; } = new();
|
|
|
|
public Stream OpenRead(string assetKey) =>
|
|
throw new InvalidOperationException("A no-op pack has no shader assets.");
|
|
}
|
|
}
|