sharpemu/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs

1079 lines
49 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Agc;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.GpuCommands;
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.Gpu.Vulkan;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using SharpEmu.Libs.Tests.Gpu.Vulkan;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
using ResourceSnapshot = SharpEmu.ShaderCompiler.Resources.ResourceSnapshot;
namespace SharpEmu.Libs.Tests.VideoOut;
// The executor over the presenter's render host on a real device: each draw renders the guest bytes it was recorded with.
[Collection(SchedulingStateCollection.Name)]
public sealed unsafe partial class RenderHostDeviceTests : IClassFixture<HeadlessVulkanFixture>
{
private const uint Size = 64;
private const uint VertexStride = 8;
private const uint VertexCount = 3;
private const uint Red = 0xFF0000FF;
private readonly HeadlessVulkan? _vulkan;
private static bool DefersGlobalBarriers =>
(bool)PresenterUnderTest.PresenterType.GetField("DeferGlobalBarriers", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!.GetValue(null)!;
[Theory]
[InlineData(false)]
[InlineData(true)]
public void OcclusionReadbackDoesNotSubmitWhileEndingRendering(bool exhaustRing)
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
var address = presenter.Harness.MapBacked(0x10000, ReadWrite);
presenter.Run(() =>
{
var ring = presenter.Harness.Cache.GetUtilityBuffer(GpuBufferUsage.Download);
if (exhaustRing)
{
Assert.True(ring.TryMap(ring.Size, out _));
ring.Commit();
}
var queryType = PresenterUnderTest.PresenterType.GetNestedType("OcclusionQuery",
System.Reflection.BindingFlags.NonPublic)!;
var query = Activator.CreateInstance(queryType, [address])!;
queryType.GetProperty("Conservative")!.SetValue(query, true);
presenter.GetField<System.Collections.IList>("_pendingOcclusionResults").Add(query);
var tick = presenter.Harness.Scheduler.CurrentTick;
presenter.InvokeMethod("PublishOcclusionResults", presenter.Command);
Assert.Equal(tick, presenter.Harness.Scheduler.CurrentTick);
presenter.Harness.Scheduler.Finish();
presenter.Harness.Scheduler.WaitForAllPriorityOperations();
((ICommandStreamHost)presenter.Instance).RunPendingCommands();
});
Assert.Equal(OcclusionResultBlock.Build(0, true, []), presenter.Harness.Read(address, 256));
presenter.Harness.Shutdown();
}
[Fact]
public void GlobalBarrierEndsDynamicRendering()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
presenter.LoadRenderingCommands();
presenter.Run(() =>
{
var state = new RenderingState { Width = 1, Height = 1, Layers = 1, Samples = 1 };
presenter.RenderHost.PrepareMemoryWritingDraw();
presenter.RenderHost.BeginRendering(in state);
((ICommandStreamHost)presenter.Instance).EmitGlobalBarrier();
Assert.False(presenter.GetField<bool>("_renderingActive"));
});
presenter.Harness.Finish();
presenter.Harness.Shutdown();
}
[Fact]
public void GlobalBarrierInAnAttachmentOnlyScopeWaitsForTheScopeToEnd()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
presenter.LoadRenderingCommands();
presenter.Run(() =>
{
var state = new RenderingState { Width = 1, Height = 1, Layers = 1, Samples = 1 };
presenter.RenderHost.BeginRendering(in state);
((ICommandStreamHost)presenter.Instance).EmitGlobalBarrier();
Assert.Equal(DefersGlobalBarriers, presenter.GetField<bool>("_renderingActive"));
Assert.Equal(DefersGlobalBarriers, presenter.GetField<bool>("_globalBarrierAfterRendering"));
presenter.RenderHost.PrepareMemoryWritingDraw();
Assert.False(presenter.GetField<bool>("_renderingActive"));
Assert.False(presenter.GetField<bool>("_globalBarrierAfterRendering"));
});
presenter.Harness.Finish();
presenter.Harness.Shutdown();
}
[Fact]
public void BeginPreparation_AcquiresMovieFrameOnceAndRetainsItUntilReplacement()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
presenter.LoadRenderingCommands();
var bridgeType = typeof(SharpEmu.Libs.Media.HostMovieBridge);
var fieldNames = new[] { "_activePath", "_activeInfo", "_frameBuffer", "_frameBufferPresented", "_playback", "_frameSerial" };
var fields = fieldNames.Select(name => bridgeType.GetField(name,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!).ToArray();
var savedValues = fields.Select(field => field.GetValue(null)).ToArray();
try
{
byte[] firstFrame = [10, 20, 30, 255];
fields[0].SetValue(null, "movie.bk2");
fields[1].SetValue(null, new SharpEmu.Libs.Media.HostMovieBridge.Bink2MovieInfo(1, 1, 30, 1));
fields[2].SetValue(null, firstFrame);
fields[3].SetValue(null, false);
fields[4].SetValue(null, null);
fields[5].SetValue(null, 100L);
presenter.Run(() =>
{
using (presenter.RenderHost.BeginPreparation())
{
Assert.Same(firstFrame, presenter.GetField<byte[]>("_hostMovieFramePixels"));
Assert.Equal(101L, presenter.GetField<long>("_hostMovieFrameSerial"));
Assert.Equal(1u, presenter.GetField<uint>("_hostMovieFrameWidth"));
}
using (presenter.RenderHost.BeginPreparation())
{
Assert.Equal(101L, presenter.GetField<long>("_hostMovieFrameSerial"));
}
fields[2].SetValue(null, null);
using (presenter.RenderHost.BeginPreparation())
{
Assert.Same(firstFrame, presenter.GetField<byte[]>("_hostMovieFramePixels"));
}
byte[] nextFrame = [40, 50, 60, 255];
fields[2].SetValue(null, nextFrame);
fields[3].SetValue(null, false);
using (presenter.RenderHost.BeginPreparation())
{
Assert.Same(nextFrame, presenter.GetField<byte[]>("_hostMovieFramePixels"));
Assert.Equal(102L, presenter.GetField<long>("_hostMovieFrameSerial"));
}
});
}
finally
{
for (var index = 0; index < fields.Length; index++) fields[index].SetValue(null, savedValues[index]);
}
presenter.Harness.Finish();
presenter.Harness.Shutdown();
}
public RenderHostDeviceTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;
// One float2 position program and one solid red pixel program over empty resource plans.
private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false,
byte[]? interpolationShader = null, uint vertexCount = VertexCount,
byte[]? vertexShader = null, uint vertexComponents = 2, uint vertexStride = VertexStride) : IShaderPipelineProvider
{
private const uint Float2Format = 64;
private static readonly uint[] UserRegisters = [0, 1];
private readonly ShaderProgramInfo _vertex = EmptyProgram(ShaderStageKind.Vertex, 1, fetchComponents: vertexComponents, userDataRegisters: pushData ? UserRegisters : null, pushDataStart: 2);
private readonly ShaderProgramInfo _pixel = EmptyProgram(ShaderStageKind.Pixel, 2, userDataRegisters: pushData ? UserRegisters : null);
private readonly ResourceSnapshot _snapshot = new() { UserData = pushData ? [0x11, 0x22] : [] };
private ShaderProgram _vertexProgram;
private ShaderProgram _pixelProgram;
internal static ShaderProgramInfo EmptyProgram(ShaderStageKind stage, ulong hash, ShaderResourceInfo? info = null, uint fetchComponents = 0, uint[]? userDataRegisters = null, uint pushDataStart = 0)
{
info ??= new ShaderResourceInfo();
var program = new ShaderProgramInfo
{
Stage = stage,
Hash = hash,
UserDataCount = (uint)(userDataRegisters?.Length ?? 0),
Resources = new SpecializedResourceInfo { Info = info },
Bindings = BindingLayout.Allocate(info, userDataRegisters ?? [], usesGlobalDataShare: false, usesFlattenedTable: false, usesShaderBase: false, pushDataStart),
};
program.VertexFetchComponents[0] = (byte)fetchComponents;
return program;
}
public bool UsesPushData => _vertex.Bindings!.UsesPushData && _pixel.Bindings!.UsesPushData;
// Modules are created on the worker with the first draw, like the cache does.
private void EnsureModules()
{
if (_vertexProgram.IsValid)
{
return;
}
_vertexProgram = new ShaderProgram(1, host.CreateShaderModule(new VulkanCompiledGuestShader(
vertexShader ?? CreatePositionVertexShader(interpolationShader is not null)), ShaderStage.Vertex, 1, 1));
_pixelProgram = new ShaderProgram(2, host.CreateShaderModule(new VulkanCompiledGuestShader(
interpolationShader ?? SpirvFixedShaders.CreateSolidFragment(1f, 0f, 0f, 1f)), ShaderStage.Pixel, 2, 2));
}
public GraphicsPrograms GetGraphicsPrograms(
VertexStageRegisters vertex,
PixelStageRegisters pixel,
ShaderInterfaceRegisters shaderInterface,
ContextRegisters context,
ReadOnlySpan<ColorComponentMap> targetExportMapping,
bool pixelActive,
bool depthBound)
{
EnsureModules();
return new()
{
Vertex = _vertexProgram,
Pixel = _pixelProgram,
VertexInput = new VertexInputInfo
{
Buffers = [new VertexInputBuffer(vertexAddress, vertexStride, vertexCount)],
Attributes = [new VertexAttributeResource(new BufferDescriptorWords((uint)vertexAddress, (uint)(vertexAddress >> 32) | (vertexStride << 16), vertexCount, (vertexComponents == 4 ? 77u : Float2Format) << 12), 0, (int)vertexComponents, 0, 0, 0, 0)],
Stage = new ShaderStageResources(_vertex, _snapshot),
},
PixelInput = new PixelInputInfo { InputCount = 0, Stage = new ShaderStageResources(_pixel, _snapshot) },
};
}
public PipelineHandle CreateGraphicsPipeline(
ReadOnlySpan<ColorTargetState> colors,
in DepthAttachmentState depth,
VertexInputInfo vertexInput,
PixelInputInfo? pixelInput,
ContextRegisters context,
in RenderingState rendering,
PrimitiveTopology topology,
bool primitiveRestartEnabled,
bool disableBlending,
ShaderProgram vertexProgram,
ShaderProgram pixelProgram) =>
host.CreateGraphicsPipeline(ShaderPipelineCache.BuildGraphicsDescription(
colors, in depth, vertexInput, pixelInput, context, in rendering, topology, primitiveRestartEnabled, disableBlending,
vertexProgram, pixelProgram, host.NoAttachmentSampleCounts));
public ComputeProgram GetComputeProgram(ComputeStageRegisters compute, ShaderInterfaceRegisters shaderInterface, uint dispatchInitiator, uint dimensionX, uint dimensionY, uint dimensionZ) =>
throw new InvalidOperationException("The test provider has no compute program.");
public PipelineHandle CreateComputePipeline(ComputeInputInfo input, ShaderProgram program) =>
throw new InvalidOperationException("The test provider has no compute pipeline.");
}
// position = (input.xy, 0, 1)
private static byte[] CreatePositionVertexShader(bool interpolationOutput = false)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var floatType = module.TypeFloat(32);
var vec2Type = module.TypeVector(floatType, 2);
var vec4Type = module.TypeVector(floatType, 4);
var inputPointer = module.TypePointer(SpirvStorageClass.Input, vec2Type);
var outputPointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var input = module.AddGlobalVariable(inputPointer, SpirvStorageClass.Input);
module.AddDecoration(input, SpirvDecoration.Location, 0);
var position = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
uint parameter = 0;
if (interpolationOutput)
{
parameter = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
module.AddDecoration(parameter, SpirvDecoration.Location, 0);
}
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddLabel();
var value = module.AddInstruction(SpirvOp.Load, vec2Type, input);
var x = module.AddInstruction(SpirvOp.CompositeExtract, floatType, value, 0);
var y = module.AddInstruction(SpirvOp.CompositeExtract, floatType, value, 1);
var composed = module.AddInstruction(
SpirvOp.CompositeConstruct, vec4Type, x, y, module.ConstantFloat(floatType, 0f), module.ConstantFloat(floatType, 1f));
module.AddStatement(SpirvOp.Store, position, composed);
if (interpolationOutput)
{
var component = module.AddInstruction(SpirvOp.FAdd, floatType,
module.AddInstruction(SpirvOp.FMul, floatType, x, module.ConstantFloat(floatType, 0.125f)),
module.ConstantFloat(floatType, 0.375f));
var output = module.AddInstruction(SpirvOp.CompositeConstruct, vec4Type,
component, component, component, component);
module.AddStatement(SpirvOp.Store, parameter, output);
}
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main",
interpolationOutput ? [input, position, parameter] : [input, position]);
return module.Build();
}
private static byte[] Triangle(float x0, float y0, float x1, float y1, float x2, float y2)
{
var bytes = new byte[VertexCount * VertexStride];
Span<float> values = [x0, y0, x1, y1, x2, y2];
for (var index = 0; index < values.Length; index++)
{
BitConverter.TryWriteBytes(bytes.AsSpan(index * sizeof(float)), values[index]);
}
return bytes;
}
private static RegisterBanks Banks(ColorTargetWords target)
{
var banks = new RegisterBanks(static message => new InvalidOperationException(message));
var context = banks.Context;
context.ColorTargets[0] = target;
context.RenderTargetMask = 0xF;
context.ShaderInterface.ColorShaderMask = 0xF;
context.ScreenViewport.Viewports[0] = new ViewportRegisters
{
XScale = Size / 2f,
XOffset = Size / 2f,
YScale = Size / 2f,
YOffset = Size / 2f,
ZScale = 0.5f,
ZOffset = 0.5f,
MaxDepth = 1,
};
banks.Shader.Vertex.ExportAddress = 0x1000;
banks.Shader.Pixel.Address = 0x2000;
banks.UserConfig.PrimitiveType = 4;
return banks;
}
private static DrawAutoArguments Draw() => new(0, 0, VertexCount, 1, 0, 0, DrawOffsetSource.Packet);
private static uint Pixel(byte[] bytes, uint x, uint y) => BitConverter.ToUInt32(bytes, (int)(((y * Size) + x) * 4));
private static CachedImage TargetImage(PresenterUnderTest presenter, ColorTargetWords words)
{
var resolution = ImageRequestBuilders.ColorTarget(words, 0xF, 0, ignoreTargetMask: false)!.Value;
return presenter.Run(() =>
{
var request = resolution.Request;
return presenter.Harness.Images.GetImage(presenter.Harness.Images.FindImage(ref request));
});
}
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(_vulkan))]
private bool Ready()
{
if (!GatePrerequisites.Ready(_vulkan))
{
return false;
}
if (!_vulkan.SupportsDynamicRendering)
{
Console.Error.WriteLine("[TEST][SKIP] The device lacks dynamic rendering or the extended dynamic state extensions.");
return false;
}
return true;
}
[Theory]
[InlineData(ShaderStageKind.Vertex, 1u)]
[InlineData(ShaderStageKind.Pixel, 1u)]
[InlineData(ShaderStageKind.Compute, 1u)]
[InlineData(ShaderStageKind.Vertex, 64u)]
[InlineData(ShaderStageKind.Pixel, 64u)]
[InlineData(ShaderStageKind.Compute, 64u)]
public void PrepareBindings_ReplacesAnIncompatibleImageType(ShaderStageKind stage, uint width)
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
using var fatal = new FatalScope();
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var info = new ShaderResourceInfo
{
Images = [new ImageResource
{
Dimension = ImageDimension.Dim2D,
ResourceClass = global::SharpEmu.ShaderCompiler.Resources.ImageResourceClass.Sampled,
NumericClass = ImageNumericClass.Float,
Read = true,
}],
};
var program = FixedProgramProvider.EmptyProgram(stage, 0x1234, info);
var words = RegisterWords.Texture(address, GuestPixelFormat.Bits8_8_8_8UNorm, width, 1);
var snapshot = new ResourceSnapshot { Images = [words] };
presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var request = ImageRequestBuilders.Texture(words, new ShaderImageShape(false, false, false, false, TextureNumericClass.Float)).Request;
var description = request.Description;
description.Type = GuestImageType.Color1D;
var identifier = harness.Images.InsertImageForTest(description);
var image = harness.Images.GetImage(identifier);
Assert.Equal(ImageType.Type1D, image.Backing.ImageType);
Assert.Equal(ImageViewType.Type2D, request.View.Type);
Assert.False(image.SupportsViewType(request.View));
var prepared = presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot));
presenter.RenderHost.BindResources(prepared);
var replacementIdentifier = harness.Images.FindImage(ref request);
Assert.NotEqual(identifier, replacementIdentifier);
Assert.Equal(ImageType.Type2D, harness.Images.GetImage(replacementIdentifier).Backing.ImageType);
Assert.True(harness.Images.GetImage(replacementIdentifier).SupportsViewType(request.View));
});
presenter.Run(presenter.RenderHost.ResetBindings);
harness.Finish();
harness.Shutdown();
}
[Theory]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
public void PrepareBindings_InvalidatesImagesOnlyForFormattedWrites(bool formatted, bool writable)
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
presenter.SetField("_minStorageBufferOffsetAlignment", 256UL);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var image = TargetImage(presenter, RegisterWords.Color(address, Size, Size));
var info = new ShaderResourceInfo { Buffers = [new BufferResource { Read = true, Written = writable, Formatted = formatted, MaxByteExtent = 0x10000 }] };
var program = FixedProgramProvider.EmptyProgram(ShaderStageKind.Compute, 3, info);
var snapshot = new ResourceSnapshot { Buffers = [[(uint)address, (uint)(address >> 32), 0x10000, 0]] };
presenter.Run(() =>
{
Assert.False(image.IsBufferModified);
using var preparation = presenter.RenderHost.BeginPreparation();
var prepared = presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot));
presenter.RenderHost.BindResources(prepared);
Assert.Equal(formatted && writable, image.IsBufferModified);
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void PrepareBindings_UnmappedBufferReportsItsDescriptorSource()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
using var fatal = new FatalScope();
var info = new ShaderResourceInfo
{
Buffers = [new BufferResource { Read = true, Source = 17, FirstUsePc = 0x4C }],
};
var program = FixedProgramProvider.EmptyProgram(ShaderStageKind.Pixel, 3, info);
var snapshot = new ResourceSnapshot
{
Buffers = [[0x600, 0xF000, 32, 0]],
UserData = [0x12345678],
};
presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var failure = Assert.Throws<SchedulerFatalException>(() =>
presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot, 0x12340000)));
Assert.Contains("address=0x0000F00000000600", failure.Message);
Assert.Contains("stage=Pixel", failure.Message);
Assert.Contains($"hash=0x{program.Hash:X16}", failure.Message);
Assert.Contains("shader=0x0000000012340000", failure.Message);
Assert.Contains("buffer=0 source=17 pc=0x4C", failure.Message);
Assert.Contains("descriptor=[00000600,0000F000,00000020,00000000]", failure.Message);
Assert.Contains("user_data=[12345678]", failure.Message);
});
presenter.Harness.Shutdown();
}
[Fact]
public void PrepareBindings_InvalidAnisotropyStillFailsWithSourceDiagnostics()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
using var fatal = new FatalScope();
var info = new ShaderResourceInfo
{
Samplers = [new SamplerResource { Source = 17, FirstUsePc = 0x64 }],
};
var program = FixedProgramProvider.EmptyProgram(ShaderStageKind.Pixel, 3, info);
var snapshot = new ResourceSnapshot
{
Samplers = [[0x65358C00, 0xC2400000, 0x002B813F, 0xA1B003AC]],
UserData = [0x12345678],
};
presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var failure = Assert.Throws<SchedulerFatalException>(() =>
presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot, 0x12340000)));
Assert.Contains("anisotropy ratio is unknown: ratio=6", failure.Message);
});
presenter.Harness.Shutdown();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ClampMappedSize_AcceptsPrivateAndBackedRangesAndStopsAtTheirEnd(bool privateMemory)
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
using var fatal = new FatalScope();
var harness = presenter.Harness;
var address = privateMemory ? harness.MapPrivate(0x10000) : harness.MapBacked(0x10000, ReadWrite);
presenter.Run(() =>
{
Assert.Equal(32UL, presenter.RenderHost.ClampMappedSize(address + 0x1DA0, 32));
Assert.Equal(16UL, presenter.RenderHost.ClampMappedSize(address + 0xFFF0, 32));
Assert.Throws<SchedulerFatalException>(() => presenter.RenderHost.ClampMappedSize(address + 0x10000, 32));
Assert.Throws<SchedulerFatalException>(() => presenter.RenderHost.ClampMappedSize(address, 0));
Assert.Throws<SchedulerFatalException>(() => presenter.RenderHost.ClampMappedSize(0, 32));
Assert.Throws<SchedulerFatalException>(() => presenter.RenderHost.ClampMappedSize(ulong.MaxValue - 15, 32));
});
harness.Shutdown();
}
[Fact]
public void ClampMappedSize_DoesNotCrossAnUnmappedGap()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
var (first, granule, last) = presenter.Harness.MapBackedSandwich();
presenter.Run(() => Assert.Equal(16UL,
presenter.RenderHost.ClampMappedSize(first + granule - 16, last - first + 16)));
presenter.Harness.Shutdown();
}
[Fact]
public void BindResourcesMakesReadOnlyDeviceAddressRangesResidentOnFirstUse()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite) + 32;
var expected = Enumerable.Range(0, 32).Select(index => (byte)(index + 17)).ToArray();
harness.Write(address, expected);
var program = FixedProgramProvider.EmptyProgram(ShaderStageKind.Compute, 3);
var snapshot = new ResourceSnapshot
{
DeviceAddressRanges = [new DeviceAddressRange(0, address, 32, true, false)],
};
GuestGpuMemoryHook.Attach(harness.Gpu);
try
{
var buffer = presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var prepared = presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot));
presenter.RenderHost.PrepareDeviceAddresses();
presenter.RenderHost.BindResources(prepared);
Assert.True(harness.Cache.IsRegionRegistered(address, 32));
Assert.False(harness.Cache.HasCpuDirtyPages(address, 32));
Assert.False(harness.Cache.HasGpuDirtyPages(address, 32));
return harness.Cache.GetBuffer(harness.Cache.FindBuffer(address, 32));
});
Assert.Equal(expected, harness.ReadBack(buffer, buffer.Offset(address), 32));
}
finally
{
GuestGpuMemoryHook.Attach(null);
}
harness.Shutdown();
}
[Fact]
public void PrepareBindings_MergesDeviceAddressRangesBeforeBuffersAreBound()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
using var fatal = new FatalScope();
presenter.SetField("_minStorageBufferOffsetAlignment", 256UL);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var written = address + 0x100;
var resource = new BufferResource { Read = true, Written = true, MaxByteExtent = 32 };
var program = FixedProgramProvider.EmptyProgram(ShaderStageKind.Compute, 3,
new ShaderResourceInfo { Buffers = [resource] });
var snapshot = new ResourceSnapshot
{
Buffers = [[(uint)written, (uint)(written >> 32), 32, 0]],
DeviceAddressRanges = [new DeviceAddressRange(0, address, 2 * GuestBufferCache.CachingPageSize, true, false)],
};
GuestGpuMemoryHook.Attach(harness.Gpu);
try
{
presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var prepared = presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot));
presenter.RenderHost.PrepareDeviceAddresses();
var owner = harness.Cache.GetBuffer(harness.Cache.FindBuffer(written, 32));
presenter.RenderHost.BindResources(prepared);
Assert.Same(owner, harness.Cache.GetBuffer(harness.Cache.FindBuffer(written, 32)));
Assert.Equal(1, harness.Cache.BufferCount);
});
}
finally
{
GuestGpuMemoryHook.Attach(null);
}
harness.Shutdown();
}
[Fact]
public void PrepareBindings_PrivateBufferUploadsCurrentBytesAndStillRejectsGpuWrites()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan!);
using var fatal = new FatalScope();
presenter.SetField("_minStorageBufferOffsetAlignment", 256UL);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
Assert.True(harness.Memory.TryAllocateAtOrAbove(0x2_0000_0000, 0x10000, executable: true, 0x4000, out var allocation));
harness.Gpu.Register(allocation, 0x10000, ReadWrite | GuestPageProtection.Execute);
var address = allocation + 0x1DA0;
var resource = new BufferResource { Read = true, MaxByteExtent = 32 };
var program = FixedProgramProvider.EmptyProgram(ShaderStageKind.Compute, 3,
new ShaderResourceInfo { Buffers = [resource] });
var snapshot = new ResourceSnapshot { Buffers = [[(uint)address, (uint)(address >> 32), 32, 0]] };
Assert.False(harness.Memory.IsBackedView(address));
Assert.True(harness.Memory.CanRead(address, 32));
GuestGpuMemoryHook.Attach(harness.Gpu);
try
{
foreach (byte seed in new byte[] { 17, 91 })
{
var expected = Enumerable.Range(0, 32).Select(index => (byte)(seed + index)).ToArray();
Assert.True(harness.Memory.TryWrite(address, expected));
var buffer = presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var prepared = presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot));
presenter.RenderHost.BindResources(prepared);
return harness.Cache.GetBuffer(harness.Cache.FindBuffer(address, 32));
});
Assert.Equal(expected, harness.ReadBack(buffer, buffer.Offset(address), 32));
Assert.Equal(HostPageProtection.ReadExecute, harness.Protection(address));
}
resource.Written = true;
presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var prepared = presenter.RenderHost.PrepareBindings(new ShaderStageResources(program, snapshot));
Assert.Throws<SchedulerFatalException>(() => presenter.RenderHost.BindResources(prepared));
});
Assert.Contains(fatal.Messages, message => message.StartsWith("Could not write the required direct backing", StringComparison.Ordinal));
Assert.False(harness.Cache.HasGpuDirtyPages(address, 32));
Assert.Equal(HostPageProtection.ReadExecute, harness.Protection(address));
}
finally
{
GuestGpuMemoryHook.Attach(null);
}
harness.Shutdown();
Assert.Equal(HostPageProtection.ReadWriteExecute, harness.Protection(address));
}
[Fact]
public void TwoDraws_RenderTheVertexVersionEachWasRecordedWith()
{
if (!Ready())
{
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var firstTarget = harness.MapBacked(0x10000, ReadWrite);
var secondTarget = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var firstWords = RegisterWords.Color(firstTarget, Size, Size);
var secondWords = RegisterWords.Color(secondTarget, Size, Size);
harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
var executor = new RenderExecutor(presenter.RenderHost, new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices));
GuestGpuMemoryHook.Attach(harness.Gpu);
try
{
presenter.Run(() => executor.DrawAuto(1, Banks(firstWords), Draw()));
// The guarded write marks the pages dirty; the second obtain uploads the new bytes.
Assert.True(harness.Memory.TryWrite(vertices, Triangle(-1f, -1f, -0.5f, -1f, -1f, -0.5f)));
presenter.Run(() => executor.DrawAuto(2, Banks(secondWords), Draw()));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var first = harness.ReadImageBytes(TargetImage(presenter, firstWords));
var second = harness.ReadImageBytes(TargetImage(presenter, secondWords));
Assert.Equal(Size * Size * 4, (uint)first.Length);
Assert.All(Enumerable.Range(0, (int)(Size * Size)), index => Assert.Equal(Red, BitConverter.ToUInt32(first, index * 4)));
Assert.Equal(Red, Pixel(second, 1, 1));
Assert.Equal(0u, Pixel(second, Size / 2, Size / 2));
Assert.Equal(0u, Pixel(second, Size - 2, Size - 2));
}
finally
{
GuestGpuMemoryHook.Attach(null);
}
harness.Shutdown();
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, 0)]
[InlineData(3, 0)]
[InlineData(0, 1)]
[InlineData(1, 1)]
[InlineData(2, 1)]
[InlineData(3, 1)]
[InlineData(0, 2)]
[InlineData(1, 2)]
[InlineData(2, 2)]
[InlineData(3, 2)]
public void RectangleList_ThreeCornersCoverTheRectangleWithoutFetchingAFourthVertex(int omittedCorner, int drawKind)
{
if (!Ready()) return;
if (!_vulkan.SupportsFillRectangle)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks VK_NV_fill_rectangle.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
(float X, float Y)[] corners = [(-1, -1), (1, -1), (-1, 1), (1, 1)];
var supplied = corners.Where((_, index) => index != omittedCorner).ToArray();
harness.Write(vertices, Triangle(supplied[0].X, supplied[0].Y,
supplied[1].X, supplied[1].Y, supplied[2].X, supplied[2].Y));
// Unrelated data immediately after the three guest vertices must not become a corner.
harness.Write(vertices + VertexCount * VertexStride, Triangle(3.0556f, -1, 0, 0, 0, 0));
var banks = Banks(words);
banks.UserConfig.PrimitiveType = 7;
banks.Context.ShaderInterface.GeometryOutputPrimitiveType = 3;
var executor = new RenderExecutor(presenter.RenderHost,
new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices));
if (drawKind != 0)
{
var indices = harness.MapBacked(0x10000, ReadWrite);
harness.Write(indices, Bytes((ushort)0, (ushort)1, (ushort)2));
var indirectArguments = drawKind == 2 ? harness.MapBacked(0x10000, ReadWrite) : 0;
if (indirectArguments != 0)
harness.Write(indirectArguments, Bytes(3u, 1u, 0u, 0u, 0u));
presenter.Run(() => executor.DrawIndexed(1, banks,
new DrawIndexedArguments(0, 0, VertexCount, indices, 0, 1, 0, 0, DrawOffsetSource.Packet,
IndirectArgumentsAddress: indirectArguments)));
}
else
{
presenter.Run(() => executor.DrawAuto(1, banks, Draw()));
}
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal((int)(Size * Size), Enumerable.Range(0, (int)(Size * Size))
.Count(index => BitConverter.ToUInt32(pixels, index * 4) == Red));
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Fact]
public void RectangleList_TwoRectanglesUseThreeVerticesEach()
{
if (!Ready()) return;
if (!_vulkan.SupportsFillRectangle)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks VK_NV_fill_rectangle.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1, -1, 0, -1, -1, 1));
harness.Write(vertices + VertexCount * VertexStride, Triangle(0, -1, 1, -1, 0, 1));
var banks = Banks(words);
banks.UserConfig.PrimitiveType = 7;
banks.Context.ShaderInterface.GeometryOutputPrimitiveType = 3;
var executor = new RenderExecutor(presenter.RenderHost,
new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices, vertexCount: 6));
presenter.Run(() => executor.DrawAuto(1, banks,
new DrawAutoArguments(0, 0, 6, 1, 0, 0, DrawOffsetSource.Packet)));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal((int)(Size * Size), Enumerable.Range(0, (int)(Size * Size))
.Count(index => BitConverter.ToUInt32(pixels, index * 4) == Red));
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Fact]
public void NewPreparation_ReusesRetiredStreamSpaceAfterTheRingFills()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var stream = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Stream);
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Write(address, new byte[0x100]);
for (var preparationIndex = 0; preparationIndex < 3; preparationIndex++)
{
presenter.Run(() =>
{
Assert.True(stream.TryMap(stream.Size, out _));
stream.Commit();
});
harness.Finish();
presenter.Run(() =>
{
using var preparation = presenter.RenderHost.BeginPreparation();
var binding = presenter.RenderHost.ObtainBuffer(address, 0x100, false);
Assert.Equal(stream.Handle.Handle, binding.Handle);
Assert.Equal(0UL, binding.Offset);
var transient = presenter.RenderHost.UploadTransient(new byte[0x100], 16);
Assert.Equal(stream.Handle.Handle, transient.Handle);
});
harness.Finish();
}
harness.Shutdown();
}
[Fact]
public void UploadTransient_TakesAHostBufferWhenTheRingWouldWrapOverThePreparation()
{
if (!Ready())
{
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var stream = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Stream);
var host = presenter.RenderHost;
var chunk = new byte[stream.Size / 4];
presenter.Run(() =>
{
using var preparation = host.BeginPreparation();
var last = default(BufferBinding);
for (var index = 0; index < 4; index++)
{
last = host.UploadTransient(chunk, 16);
Assert.Equal(stream.Handle.Handle, last.Handle);
}
var overflow = host.UploadTransient(chunk, 16);
Assert.NotEqual(stream.Handle.Handle, overflow.Handle);
Assert.NotEqual(0UL, overflow.Handle);
Assert.Equal(0UL, overflow.Offset);
});
harness.Finish();
harness.Shutdown();
}
// Retained ring bytes force expanded indices into a host buffer; the draw reads it before retirement.
[Fact]
public void OverflowIndexBuffer_IsConsumedByTheDrawAndRetiredWithIt()
{
if (!Ready())
{
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var indices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
harness.Write(indices, [0, 1, 2]);
var stream = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Stream);
presenter.Run(() =>
{
Assert.True(stream.TryMap(stream.Size, out _, 16));
stream.Commit();
});
using var retention = stream.RetainContents();
var executor = new RenderExecutor(presenter.RenderHost, new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices));
presenter.Run(() => executor.DrawIndexed(1, Banks(words), new DrawIndexedArguments(0, 0, VertexCount, indices, 2, 1, 0, 0, DrawOffsetSource.Packet)));
Assert.Equal(0UL, presenter.HostBuffers.CachedBytes);
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
presenter.Run(() => presenter.InvokeMethod("WaitForAllGuestSubmissions"));
Assert.Equal(8UL, presenter.HostBuffers.CachedBytes);
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal(Red, Pixel(pixels, Size / 2, Size / 2));
harness.Shutdown();
}
[Fact]
public void PresenterDisposal_ReleasesPipelinesAndLayoutsAfterRecordedDraw()
{
if (!Ready()) return;
var presenter = new PresenterUnderTest(_vulkan);
var pipelines = presenter.GetField<System.Collections.IDictionary>("_pipelineEntries");
var layouts = presenter.GetField<System.Collections.IDictionary>("_shaderModules");
using (presenter)
{
presenter.LoadRenderingCommands();
var target = presenter.Harness.MapBacked(0x10000, ReadWrite);
var vertices = presenter.Harness.MapBacked(0x10000, ReadWrite);
presenter.Harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
var words = RegisterWords.Color(target, Size, Size);
var executor = new RenderExecutor(presenter.RenderHost,
new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices));
presenter.Run(() => executor.DrawAuto(1, Banks(words), Draw()));
Assert.NotEmpty(pipelines);
Assert.NotEmpty(layouts);
}
Assert.Empty(pipelines);
Assert.Empty(layouts);
_vulkan.AssertNoValidationMessages();
}
// Both stages push their blocks into the one 128-byte range; the validation layer checks the layout covers them.
[Fact]
public void PushConstants_CoverBothStagesOfADrawInOneRange()
{
if (!Ready())
{
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices, pushData: true);
Assert.True(provider.UsesPushData);
var executor = new RenderExecutor(presenter.RenderHost, provider);
presenter.Run(() => executor.DrawAuto(1, Banks(words), Draw()));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal(Red, Pixel(pixels, Size / 2, Size / 2));
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GuestWordReaders_RejectUnmappedMemoryAndClearTheResult(bool cleanOnly)
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan);
var host = (IShaderPipelineHost)presenter.Instance;
presenter.Run(() =>
{
const ulong unmappedAddress = 0x12340000;
uint word = uint.MaxValue;
var success = cleanOnly
? host.TryReadCleanGuestWord(unmappedAddress, out word)
: host.TryReadGuestWord(unmappedAddress, out word);
Assert.False(success);
Assert.Equal(0u, word);
});
}
// The plain reader downloads a GPU-written range first; the clean reader refuses it until the GPU is done.
[Fact]
public void GuestWordReaders_HonourGpuOwnershipOfTheRange()
{
if (!Ready())
{
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
harness.Write(address, BitConverter.GetBytes(0xCAFEF00Du));
var host = (IShaderPipelineHost)presenter.Instance;
GuestGpuMemoryHook.Attach(harness.Gpu);
try
{
presenter.Run(() =>
{
Assert.True(host.TryReadGuestWord(address, out var word));
Assert.Equal(0xCAFEF00Du, word);
Assert.True(host.TryReadCleanGuestWord(address, out word));
Assert.Equal(0xCAFEF00Du, word);
_ = harness.Cache.ObtainBuffer(address, 0x1000, isWritten: true);
Assert.False(host.TryReadCleanGuestWord(address, out _));
Assert.False(host.TryReadCleanGuestWord(address + 0x800, out _));
Assert.True(host.TryReadCleanGuestWord(address + 0x2000, out _));
Assert.True(host.TryReadGuestWord(address, out word));
Assert.Equal(0xCAFEF00Du, word);
});
}
finally
{
GuestGpuMemoryHook.Attach(null);
}
harness.Finish();
harness.Shutdown();
}
[Fact]
public void ConsecutiveDraws_ShareOneRenderingScopeAndCloseItBeforeTheSubmit()
{
if (!Ready())
{
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
var executor = new RenderExecutor(presenter.RenderHost, new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices));
var rendering = (IRenderingState)presenter.Instance;
presenter.Run(() =>
{
executor.DrawAuto(1, Banks(words), Draw());
Assert.True(rendering.IsRendering);
executor.DrawAuto(2, Banks(words), Draw());
Assert.True(rendering.IsRendering);
Assert.Equal(1L, presenter.GetField<long>("_renderingScopesBegun"));
presenter.InvokeMethod("FlushBatchedGuestCommands");
Assert.False(rendering.IsRendering);
});
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
Assert.Equal(Red, Pixel(pixels, Size / 2, Size / 2));
harness.Shutdown();
}
}