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

797 lines
42 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections;
using System.Reflection;
using System.Runtime.CompilerServices;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.GpuCommands;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Tests.Gpu.Buffers;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using SharpEmu.Libs.Tests.Gpu.Vulkan;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Agc;
using SharpEmu.Libs.Gpu.Vulkan;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
using ImageResourceClass = SharpEmu.Libs.Gpu.Rendering.ImageResourceClass;
using PlanImageResourceClass = SharpEmu.ShaderCompiler.Resources.ImageResourceClass;
using ResourceSnapshot = SharpEmu.ShaderCompiler.Resources.ResourceSnapshot;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
namespace SharpEmu.Libs.Tests.VideoOut;
// The presenter's image bindings over the store: discovery, acquisition, layouts, per-draw passes and shutdown.
[Collection(SchedulingStateCollection.Name)]
public sealed class PresenterImageBindingTests : IClassFixture<HeadlessVulkanFixture>
{
private const BindingFlags InstanceMembers = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private static readonly Type PresenterType = typeof(VulkanVideoPresenter).GetNestedType("Presenter", BindingFlags.NonPublic)!;
private static readonly Type ColorAttachmentType = typeof(VulkanVideoPresenter).GetNestedType("ColorAttachment", BindingFlags.NonPublic)!;
private static readonly Type TextureResourceType = typeof(VulkanVideoPresenter).GetNestedType("TextureResource", BindingFlags.NonPublic)!;
private static readonly ShaderImageShape Sampled2D = new(false, false, false, false, TextureNumericClass.Float);
private readonly HeadlessVulkan? _vulkan;
public PresenterImageBindingTests(HeadlessVulkanFixture fixture) => _vulkan = fixture.Vulkan;
[Theory]
[InlineData(GuestPixelFormat.Bits16UNorm, Format.D16Unorm)]
[InlineData(GuestPixelFormat.Bits32Float, Format.D32Sfloat)]
public void ComparisonBinding_CreatesDepthImageWithoutAnAttachment(GuestPixelFormat guestFormat, Format hostFormat)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var fatal = new FatalScope();
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var sample = guestFormat == GuestPixelFormat.Bits16UNorm
? BitConverter.GetBytes((ushort)0x4000)
: BitConverter.GetBytes(0.25f);
var guestBytes = new byte[0x10000];
for (var offset = 0; offset < guestBytes.Length; offset += sample.Length)
sample.CopyTo(guestBytes, offset);
harness.Write(address, guestBytes);
CachedImage sampledImage = null!;
presenter.Run(() =>
{
var resource = new ImageResource
{
ResourceClass = PlanImageResourceClass.Sampled,
NumericClass = ImageNumericClass.Float,
Dimension = ImageDimension.Dim2D,
Read = true,
DepthCompare = true,
};
var words = RegisterWords.Texture(address, guestFormat, 48, 24);
words[3] = (words[3] & ~0xFFFu) | ViewFormatRules.PackDestinationSelect(4, 0, 0, 1);
var binding = presenter.InvokeMethod("ResolveImageBinding", resource, words, new ShaderProgramInfo(), 0)!;
var identifier = (ResourceSlotIdentifier)GetFieldValue(binding, "ImageIdentifier");
var request = (ImageRequest)GetFieldValue(binding, "Request");
var image = harness.Images.GetImage(identifier);
sampledImage = image;
var view = harness.Images.AcquireTextureView(identifier, request);
Assert.Equal(hostFormat, image.Backing.Format);
Assert.True(image.Description.IsDepth);
Assert.NotEqual(0UL, view.Handle);
Assert.Contains(image.Views, candidate => candidate.Description.Aspect == ImageAspectFlags.DepthBit);
presenter.RenderHost.ResetBindings();
});
var uploaded = harness.ReadImageBytes(sampledImage, ImageAspectFlags.DepthBit);
Assert.Equal(sample, uploaded.AsSpan(0, sample.Length).ToArray());
harness.Shutdown();
}
[Theory]
[InlineData(true, 1u, false)]
[InlineData(true, 2u, true)]
[InlineData(false, 1u, false)]
public unsafe void StencilStorage_PublishesRecordedWritesWithoutChangingDepth(bool recordDispatch, uint layers, bool sampledAlias)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
if (!_vulkan.StorageImageExtendedFormats || !_vulkan.SupportsDynamicRendering)
{
Assert.False(ReferenceShaders.Required, "Stencil storage tests need extended storage formats and dynamic rendering.");
return;
}
using var fatal = new FatalScope();
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var region = harness.MapBacked(0x100000, ReadWrite);
var stencilAddress = region + 0x80000;
CachedImage attachment = null!;
var workingImages = new List<CachedImage>();
presenter.Run(() =>
{
var shape = new ShaderImageShape(false, true, true, false, TextureNumericClass.Uint);
var texture = new GuestDrawTexture(stencilAddress, 64, 64, 0, 0, [], false, true,
Descriptor: RegisterWords.Texture(stencilAddress, GuestPixelFormat.Bits8UInt, 64, 64,
type: GuestImageType.Color2DArray, tile: GuestTileMode.Depth, layers: layers, baseArray: layers - 1), Shape: shape);
// The stencil range was a color image before the depth target took ownership.
presenter.InvokeMethod("ResolveTexture", texture);
var target = new GuestDepthTarget(region, region, 64, 64, 3, 0, 1f, false,
Registers: RegisterWords.Depth(region, 64, 64, stencilBase: stencilAddress) with { DepthView = (layers - 1) << 13 });
var depth = presenter.InvokeMethod("DiscoverDepthTarget", target)!;
presenter.InvokeMethod("AcquireDepthAttachment", depth, GuestDepthState.Default);
attachment = (CachedImage)GetFieldValue(depth, "Image");
var range = new ImageSubresourceRange(ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit, 0, 1, 0, layers);
var initial = new ClearDepthStencilValue(0.25f, 0x37);
attachment.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, null, presenter.Command);
_vulkan.Vk.CmdClearDepthStencilImage(presenter.Command, attachment.Backing.Handle, ImageLayout.TransferDstOptimal, &initial, 1, &range);
presenter.RenderHost.ResetBindings();
var storageImage = new ImageResource { ResourceClass = PlanImageResourceClass.Storage, NumericClass = ImageNumericClass.Uint, Dimension = ImageDimension.Dim2DArray, Read = true, Written = true };
var sampledImage = new ImageResource { ResourceClass = PlanImageResourceClass.Sampled, NumericClass = ImageNumericClass.Uint, Dimension = ImageDimension.Dim2DArray, Read = true };
var info = new ShaderResourceInfo { Images = sampledAlias ? [sampledImage, storageImage] : [storageImage] };
var storageBinding = BindingLayout.NativeBindingIndex(ShaderStage.Compute, ImageDescriptorBinding.ForImage(storageImage)!.Value);
var program = new ShaderProgramInfo
{
Stage = ShaderStageKind.Compute,
Images = sampledAlias
? [new ImageResourceInfo(ImageResourceClass.Sampled, false), new ImageResourceInfo(ImageResourceClass.Storage, true)]
: [new ImageResourceInfo(ImageResourceClass.Storage, true)],
Resources = new SpecializedResourceInfo { Info = info },
Bindings = BindingLayout.Allocate(info, [], usesGlobalDataShare: false, usesFlattenedTable: false, usesShaderBase: false),
};
var words = texture.Descriptor!;
var stage = new ShaderStageResources(program, new ResourceSnapshot { Images = sampledAlias ? [words, words] : [words] });
var input = new ComputeInputInfo { ThreadsX = 1, ThreadsY = 1, ThreadsZ = 1, Stage = stage };
var pipelineHost = (IShaderPipelineHost)presenter.Instance;
var module = pipelineHost.CreateShaderModule(new VulkanCompiledGuestShader(CreateStencilIncrementShader(storageBinding)), ShaderStage.Compute, 0, 1);
Array? recycledTextures = null;
for (var dispatchIndex = 0; dispatchIndex < 2; dispatchIndex++)
{
using (presenter.RenderHost.BeginPreparation())
{
var bindings = presenter.RenderHost.PrepareBindings(stage);
presenter.RenderHost.BindResources(bindings);
var textures = (Array)bindings.GetType().GetProperty("Textures")!.GetValue(bindings)!;
if (recycledTextures is not null) Assert.Same(recycledTextures, textures);
recycledTextures = textures;
var working = (CachedImage)GetFieldValue(textures.GetValue(textures.Length - 1)!, "CachedImage");
workingImages.Add(working);
Assert.NotSame(attachment, working);
Assert.Equal(Format.R8Uint, working.Backing.Format);
Assert.NotEqual(0, (int)(working.Backing.Usage & ImageUsageFlags.StorageBit));
Assert.Equal(0, (int)(attachment.Backing.Usage & ImageUsageFlags.StorageBit));
var invalidStorage = ImageViewDescription.Default with
{
Format = Format.R8Uint, Aspect = ImageAspectFlags.StencilBit, Usage = ImageUsageFlags.StorageBit,
};
Assert.Throws<SchedulerFatalException>(() => attachment.GetOrCreateView(invalidStorage));
if (recordDispatch)
{
var pipeline = pipelineHost.CreateComputePipeline(new ComputePipelineDescription { Input = input, Program = new ShaderProgram(1, module), Stage = program });
presenter.RenderHost.CommitBindings(PipelineBindPoint.Compute, pipeline, [bindings]);
if (sampledAlias)
Assert.Same(working, GetFieldValue(textures.GetValue(0)!, "CachedImage"));
presenter.RenderHost.BindPipeline(PipelineBindPoint.Compute, pipeline);
presenter.RenderHost.Dispatch(32, 64, 1);
presenter.RenderHost.ShaderAccessBarrier();
}
else
{
// An abandoned preparation must not publish even if its working image changed.
working.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, null, presenter.Command);
var discarded = new ClearColorValue(0x55u, 0u, 0u, 0u);
var colorRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, layers);
_vulkan.Vk.CmdClearColorImage(presenter.Command, working.Backing.Handle, ImageLayout.TransferDstOptimal, &discarded, 1, &colorRange);
}
}
// CPU descriptions are returned immediately; the GPU resources
// they described remain live until the submission completes.
Assert.All(recycledTextures!.Cast<object?>(), Assert.Null);
Assert.NotEqual(0UL, workingImages[^1].Backing.Handle.Handle);
presenter.RenderHost.ResetBindings();
}
});
harness.Finish();
Assert.All(workingImages, image => Assert.False(image.Backing.Exists));
var stencil = harness.ReadImageBytes(attachment, ImageAspectFlags.StencilBit);
for (var layer = 0; layer < layers; layer++)
for (var row = 0; row < 64; row++)
for (var column = 0; column < 64; column++)
Assert.Equal((byte)(recordDispatch && layer == layers - 1 && column < 32 ? 0x39 : 0x37), stencil[layer * 64 * 64 + row * 64 + column]);
var depthBytes = harness.ReadImageBytes(attachment, ImageAspectFlags.DepthBit);
for (var offset = 0; offset < depthBytes.Length; offset += sizeof(float))
Assert.Equal(0.25f, BitConverter.ToSingle(depthBytes, offset));
Assert.True(attachment.IsGpuModified);
Assert.True(harness.ProxyAt(stencilAddress, attachment.Description.Stencil.Size).IsValid);
harness.Shutdown();
}
private static byte[] CreateStencilIncrementShader(uint imageBinding)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
module.AddCapability(SpirvCapability.StorageImageExtendedFormats);
var voidType = module.TypeVoid();
var unsignedType = module.TypeInt(32, false);
var coordinateType = module.TypeVector(unsignedType, 3);
var pixelType = module.TypeVector(unsignedType, 4);
var imageType = module.TypeImage(unsignedType, SpirvImageDim.Dim2D, false, true, false, 2, SpirvImageFormat.R8ui);
var image = module.AddGlobalVariable(module.TypePointer(SpirvStorageClass.UniformConstant, imageType), SpirvStorageClass.UniformConstant);
module.AddDecoration(image, SpirvDecoration.DescriptorSet, 0);
module.AddDecoration(image, SpirvDecoration.Binding, imageBinding);
var position = module.AddGlobalVariable(module.TypePointer(SpirvStorageClass.Input, coordinateType), SpirvStorageClass.Input);
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.GlobalInvocationId);
var zero = module.Constant(unsignedType, 0);
var main = module.BeginFunction(voidType, module.TypeFunction(voidType));
module.AddLabel();
var coordinates = module.AddInstruction(SpirvOp.Load, coordinateType, position);
var loadedImage = module.AddInstruction(SpirvOp.Load, imageType, image);
var pixel = module.AddInstruction(SpirvOp.ImageRead, pixelType, loadedImage, coordinates);
var previous = module.AddInstruction(SpirvOp.CompositeExtract, unsignedType, pixel, 0);
var incremented = module.AddInstruction(SpirvOp.IAdd, unsignedType, previous, module.Constant(unsignedType, 1));
var result = module.AddInstruction(SpirvOp.CompositeConstruct, pixelType, incremented, zero, zero, zero);
module.AddStatement(SpirvOp.ImageWrite, loadedImage, coordinates, result);
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(SpirvExecutionModel.GLCompute, main, "main", [image, position]);
module.AddExecutionMode(main, SpirvExecutionMode.LocalSize, 1, 1, 1);
return module.Build();
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, true, false)]
[InlineData(true, false, false)]
[InlineData(false, false, true)]
[InlineData(true, false, true)]
[InlineData(false, true, true)]
[InlineData(false, false, false, true)]
[InlineData(false, true, false, true)]
[InlineData(true, false, true, true)]
[InlineData(true, false, false, true)]
[InlineData(false, true, true, true)]
public void SampledDepth_PreservesAttachmentLayoutOrRejectsOverlappingWrites(bool depthWrite, bool stencilWrite, bool sampleStencil, bool pendingClear = false)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var region = harness.MapBacked(0x100000, ReadWrite);
CachedImage? clearedImage = null;
var target = new GuestDepthTarget(region, region, 64, 64, 3, 0, 1f, false,
Registers: RegisterWords.Depth(region, 64, 64, stencilBase: region + 0x80000));
presenter.Run(() =>
{
var depth = presenter.InvokeMethod("DiscoverDepthTarget", target)!;
var state = new GuestDepthState(true, depthWrite, 7, StencilTestEnable: stencilWrite,
StencilFront: new GuestStencilFaceState(1, 1, 1, 7, 0xFF, 0xFF, 0, 0));
presenter.InvokeMethod("AcquireDepthAttachment", depth, state);
if (pendingClear)
{
depth.GetType().GetField("ClearDepth")!.SetValue(depth, true);
depth.GetType().GetField("ClearStencil")!.SetValue(depth, true);
depth.GetType().GetField("ClearStencilValue")!.SetValue(depth, (byte)0x40);
}
var image = (CachedImage)GetFieldValue(depth, "Image");
var request = (ImageRequest)GetFieldValue(depth, "Request");
request.Role = ImageRole.Texture;
request.View = request.View with
{
Format = sampleStencil ? Format.R8Uint : Format.R32Sfloat,
Aspect = sampleStencil ? ImageAspectFlags.StencilBit : ImageAspectFlags.DepthBit,
Usage = ImageUsageFlags.SampledBit,
};
var binding = Activator.CreateInstance(TextureResourceType, nonPublic: true)!;
TextureResourceType.GetField("CachedImage")!.SetValue(binding, image);
TextureResourceType.GetField("Request")!.SetValue(binding, request);
TextureResourceType.GetField("View")!.SetValue(binding, image.GetOrCreateView(request.View));
var bindings = Array.CreateInstance(TextureResourceType, 1);
bindings.SetValue(binding, 0);
if (sampleStencil ? stencilWrite : depthWrite)
{
var error = Assert.Throws<InvalidOperationException>(() => presenter.InvokeMethod("RecordDrawTextureTransitions", bindings, depth, state));
Assert.Contains($"sampled_aspects={(sampleStencil ? ImageAspectFlags.StencilBit : ImageAspectFlags.DepthBit)}", error.Message);
Assert.Contains("clear_depth=False clear_stencil=False", error.Message);
Assert.Contains("depth_state=", error.Message);
}
else
{
presenter.InvokeMethod("RecordDrawTextureTransitions", bindings, depth, state);
Assert.False((bool)GetFieldValue(depth, "ClearDepth"));
Assert.False((bool)GetFieldValue(depth, "ClearStencil"));
Assert.Equal(GetFieldValue(depth, "Layout"), GetFieldValue(binding, "Layout"));
harness.Scheduler.Finish();
if (pendingClear)
clearedImage = image;
}
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
if (clearedImage is not null)
{
var depthBytes = harness.ReadImageBytes(clearedImage, ImageAspectFlags.DepthBit);
for (var offset = 0; offset < depthBytes.Length; offset += sizeof(float))
Assert.Equal(1f, BitConverter.ToSingle(depthBytes, offset));
var stencilBytes = harness.ReadImageBytes(clearedImage, ImageAspectFlags.StencilBit);
Assert.All(stencilBytes.Take(64 * 64), value => Assert.Equal((byte)0x40, value));
}
harness.Shutdown();
}
[Theory]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(true, true)]
public unsafe void SeparateDepthClear_PreservesOtherAspectsAndAllowsLargerColorExtent(bool clearDepth, bool clearStencil)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var region = harness.MapBacked(0x100000, ReadWrite);
CachedImage depthImage = null!;
CachedImage colorImage = null!;
presenter.Run(() =>
{
var depthTarget = new GuestDepthTarget(region, region, 64, 64, 3, 0, 0.25f, false,
Registers: RegisterWords.Depth(region, 64, 64, stencilBase: region + 0x80000));
var depth = presenter.InvokeMethod("DiscoverDepthTarget", depthTarget)!;
presenter.InvokeMethod("AcquireDepthAttachment", depth, GuestDepthState.Default);
depthImage = (CachedImage)GetFieldValue(depth, "Image");
depthImage.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, null, presenter.Command);
var range = new ImageSubresourceRange(ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit, 0, 1, 0, 1);
var initial = new ClearDepthStencilValue(0.75f, 0x37);
harness.Vulkan.Vk.CmdClearDepthStencilImage(presenter.Command, depthImage.Backing.Handle, ImageLayout.TransferDstOptimal, &initial, 1, &range);
depth.GetType().GetField("ClearDepth")!.SetValue(depth, clearDepth);
depth.GetType().GetField("ClearStencil")!.SetValue(depth, clearStencil);
depth.GetType().GetField("ClearStencilValue")!.SetValue(depth, (byte)0x6A);
presenter.InvokeMethod("RecordSeparateDepthClear", depth);
var color = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(region + 0x40000, 128, 128), false, false)!;
presenter.InvokeMethod("AcquireColorAttachment", color);
color.GetType().GetField("Clear")!.SetValue(color, true);
colorImage = (CachedImage)GetFieldValue(color, "Image");
var extent = new Extent2D(128, 128);
ClearColorAttachment(presenter, color, extent, 1, new ClearColorValue(1f, 0f, 0f, 1f));
harness.Scheduler.Finish();
presenter.InvokeMethod("ResetImageBindings");
});
var depthBytes = harness.ReadImageBytes(depthImage, ImageAspectFlags.DepthBit);
for (var offset = 0; offset < depthBytes.Length; offset += sizeof(float))
Assert.Equal(clearDepth ? 0.25f : 0.75f, BitConverter.ToSingle(depthBytes, offset));
var stencilBytes = harness.ReadImageBytes(depthImage, ImageAspectFlags.StencilBit);
Assert.All(stencilBytes.Take(64 * 64), value => Assert.Equal(clearStencil ? (byte)0x6A : (byte)0x37, value));
var colorBytes = harness.ReadImageBytes(colorImage);
Assert.Equal(128 * 128 * 4, colorBytes.Length);
for (var offset = 0; offset < colorBytes.Length; offset += sizeof(uint))
Assert.Equal(0xFF0000FFu, BitConverter.ToUInt32(colorBytes, offset));
harness.Shutdown();
}
private static void ClearColorAttachment(PresenterUnderTest presenter, object attachment, Extent2D extent, uint layers, ClearColorValue clear)
{
presenter.LoadRenderingCommands();
var state = new RenderingState
{
Width = extent.Width,
Height = extent.Height,
Layers = layers,
Samples = 1,
ColorAttachmentCount = 1,
};
state.ColorAttachments[0] = new RenderingAttachment(
(ImageView)GetFieldValue(attachment, "View"),
(ImageLayout)GetFieldValue(attachment, "Layout"),
((ImageRequest)GetFieldValue(attachment, "Request")).View.Format,
clear.Uint32_0, clear.Uint32_1, clear.Uint32_2, clear.Uint32_3,
true, false, false, false, false);
presenter.RenderHost.BeginRendering(in state);
presenter.RenderHost.EndRendering();
}
private static object GetFieldValue(object target, string name) => target.GetType().GetField(name, InstanceMembers)!.GetValue(target)!;
private static GuestRenderTarget ColorTarget(ulong address, uint width = 64, uint height = 64, uint sliceMax = 0, uint sliceStart = 0) =>
new(address, width, height, 0, 0, Registers: RegisterWords.Color(address, width, height, sliceMax: sliceMax, sliceStart: sliceStart));
private static GuestDrawTexture Texture(ulong address) =>
new(address, 64, 64, 0, 0, [], false, false, Descriptor: RegisterWords.Texture(address, GuestPixelFormat.Bits8_8_8_8UNorm, 64, 64), Shape: Sampled2D);
// Binds one shader image the way a draw does and returns the bound resource.
private static object AcquireTexture(PresenterUnderTest presenter, GuestDrawTexture texture)
{
var resource = presenter.InvokeMethod("ResolveTexture", texture)!;
var bindings = Array.CreateInstance(TextureResourceType, 1);
bindings.SetValue(resource, 0);
presenter.InvokeMethod("AcquireTextureViews", bindings, new List<GuestDrawTexture> { texture });
return bindings.GetValue(0)!;
}
[Theory]
[InlineData(TextureNumericClass.Float, false)]
[InlineData(TextureNumericClass.Uint, false)]
[InlineData(TextureNumericClass.Sint, false)]
[InlineData(TextureNumericClass.Float, true)]
[InlineData(TextureNumericClass.Uint, true)]
[InlineData(TextureNumericClass.Sint, true)]
public void NullTexture_ResolvesAndAcquiresAView(TextureNumericClass numericClass, bool storage)
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var texture = new GuestDrawTexture(0, 1, 1, 0, 0, [], false, storage,
Descriptor: [], Shape: new ShaderImageShape(false, false, storage, false, numericClass));
var resource = presenter.Run(() => AcquireTexture(presenter, texture));
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(resource, "View")).Handle);
var request = (ImageRequest)GetFieldValue(resource, "Request");
var expected = numericClass switch
{
TextureNumericClass.Uint => Format.R32Uint,
TextureNumericClass.Sint => Format.R32Sint,
_ => Format.R32Sfloat,
};
Assert.Equal(expected, request.View.Format);
presenter.Run(() => presenter.InvokeMethod("ResetImageBindings"));
presenter.Harness.Finish();
presenter.Harness.Shutdown();
}
[Fact]
public void NullTexture_ForDepthReferenceSampling_IsADepthView()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var texture = new GuestDrawTexture(0, 1, 1, 0, 0, [], false, false,
Descriptor: [], Shape: new ShaderImageShape(false, false, false, false, TextureNumericClass.Float) with { DepthCompare = true });
var resource = presenter.Run(() => AcquireTexture(presenter, texture));
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(resource, "View")).Handle);
var request = (ImageRequest)GetFieldValue(resource, "Request");
Assert.Equal(Format.D32Sfloat, request.View.Format);
presenter.Run(() => presenter.InvokeMethod("ResetImageBindings"));
presenter.Harness.Finish();
presenter.Harness.Shutdown();
}
[Fact]
public void ColorTarget_IsFoundInTheStoreAndBoundForTheDraw()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var attachment = presenter.Run(() => presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address), false, false));
Assert.NotNull(attachment);
var imageIdentifier = (ResourceSlotIdentifier)GetFieldValue(attachment, "ImageIdentifier");
var image = harness.Image(imageIdentifier);
Assert.True(image.Binding.IsTarget);
Assert.Equal(address, image.Description.Data.Address);
Assert.Equal(new Extent3D(64, 64, 1), image.Description.Extent);
Assert.Equal(Format.R8G8B8A8Unorm, image.Description.PixelFormat);
presenter.Run(() => presenter.InvokeMethod("AcquireColorAttachment", attachment));
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(attachment, "View")).Handle);
Assert.Equal(ImageLayout.ColorAttachmentOptimal, (ImageLayout)GetFieldValue(attachment, "Layout"));
Assert.False((bool)GetFieldValue(attachment, "Clear"));
Assert.Same(image, GetFieldValue(attachment, "Image"));
presenter.Run(() => presenter.InvokeMethod("ResetImageBindings"));
Assert.False(harness.Image(imageIdentifier).Binding.IsTarget);
harness.Finish();
harness.Shutdown();
}
[Fact]
public void TargetSampledByItsOwnDraw_UsesTheGeneralLayout()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var texture = Texture(address);
presenter.Run(() =>
{
var resource = presenter.InvokeMethod("ResolveTexture", texture)!;
var attachment = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address), false, false)!;
Assert.Equal(GetFieldValue(resource, "ImageIdentifier"), GetFieldValue(attachment, "ImageIdentifier"));
var image = harness.Image((ResourceSlotIdentifier)GetFieldValue(resource, "ImageIdentifier"));
Assert.True(image.Binding.IsBound);
Assert.True(image.Binding.IsTarget);
Assert.False(image.Binding.ShaderWrite);
presenter.InvokeMethod("AcquireColorAttachment", attachment);
Assert.Equal(ImageLayout.General, (ImageLayout)GetFieldValue(attachment, "Layout"));
var bindings = Array.CreateInstance(TextureResourceType, 1);
bindings.SetValue(resource, 0);
presenter.InvokeMethod("AcquireTextureViews", bindings, new List<GuestDrawTexture> { texture });
presenter.InvokeMethod("RecordTextureTransitions", bindings);
var bound = bindings.GetValue(0)!;
Assert.Equal(ImageLayout.General, (ImageLayout)GetFieldValue(bound, "Layout"));
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(bound, "View")).Handle);
Assert.NotEqual(0UL, ((Sampler)GetFieldValue(bound, "Sampler")).Handle);
Assert.True(image.Uses.Texture);
presenter.InvokeMethod("ResetImageBindings");
Assert.False(image.Binding.IsBound);
Assert.False(image.Binding.IsTarget);
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void DepthTarget_LayoutFollowsTheWrittenAspects()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var region = harness.MapBacked(0x100000, ReadWrite);
var words = RegisterWords.Depth(region, 64, 64, stencilBase: region + 0x80000);
var target = new GuestDepthTarget(region, region, 64, 64, 3, 0, 1f, false, Registers: words);
presenter.Run(() =>
{
var depth = presenter.InvokeMethod("DiscoverDepthTarget", target);
Assert.NotNull(depth);
var imageIdentifier = (ResourceSlotIdentifier)GetFieldValue(depth, "ImageIdentifier");
Assert.True(harness.Image(imageIdentifier).Binding.IsTarget);
Assert.Equal(Format.D32SfloatS8Uint, harness.Image(imageIdentifier).Description.PixelFormat);
presenter.InvokeMethod("AcquireDepthAttachment", depth, new GuestDepthState(true, true, 7));
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(depth, "View")).Handle);
Assert.Equal(ImageLayout.DepthAttachmentStencilReadOnlyOptimal, (ImageLayout)GetFieldValue(depth, "Layout"));
Assert.False((bool)GetFieldValue(depth, "ClearDepth"));
presenter.InvokeMethod("AcquireDepthAttachment", depth, new GuestDepthState(true, false, 7));
Assert.Equal(ImageLayout.DepthStencilReadOnlyOptimal, (ImageLayout)GetFieldValue(depth, "Layout"));
var stencilWrite = new GuestDepthState(true, true, 7, StencilTestEnable: true, StencilFront: new GuestStencilFaceState(1, 1, 1, 7, 0xFF, 0xFF, 0, 0));
presenter.InvokeMethod("AcquireDepthAttachment", depth, stencilWrite);
Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, (ImageLayout)GetFieldValue(depth, "Layout"));
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void DepthTarget_ConsumesQueuedMetadataClearOnce()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var region = harness.MapBacked(0x100000, ReadWrite);
var metadataAddress = region + 0x80000;
var words = RegisterWords.Depth(region, 64, 64) with
{
ZInfo = 3u | (1u << 29),
HtileBase = metadataAddress,
};
var target = new GuestDepthTarget(region, region, 64, 64, 3, 0, 0f, false,
HtileAddress: metadataAddress, HtileAcceleration: true, MetadataClear: true, Registers: words);
presenter.Run(() =>
{
var depth = presenter.InvokeMethod("DiscoverDepthTarget", target)!;
presenter.InvokeMethod("AcquireDepthAttachment", depth, new GuestDepthState(true, false, 7));
Assert.True((bool)GetFieldValue(depth, "ClearDepth"));
Assert.Equal(0f, (float)GetFieldValue(depth, "ClearDepthValue"));
Assert.False((bool)GetFieldValue(depth, "ClearStencil"));
Assert.False(harness.Images.IsMetadataCleared(metadataAddress, 0));
presenter.InvokeMethod("ResetImageBindings");
depth = presenter.InvokeMethod("DiscoverDepthTarget", target with { MetadataClear = false })!;
presenter.InvokeMethod("AcquireDepthAttachment", depth, new GuestDepthState(true, false, 7));
Assert.False((bool)GetFieldValue(depth, "ClearDepth"));
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void StorageBinding_SelectsTheInstructionMipAboveTheDescriptorBase()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
var storage = Sampled2D with { Storage = true };
GuestDrawTexture Storage(uint mip, uint baseLevel = 0) => new(
address, 64, 64, 0, 0, [], false, true, MipLevel: mip, BaseMipLevel: baseLevel,
Descriptor: RegisterWords.Texture(address, GuestPixelFormat.Bits8_8_8_8UNorm, 64, 64, baseLevel: baseLevel, lastLevel: 2, maxMip: 2), Shape: storage);
presenter.Run(() =>
{
var level1 = AcquireTexture(presenter, Storage(1));
var level1View = ((ImageRequest)GetFieldValue(level1, "Request")).View;
Assert.Equal(1u, level1View.BaseLevel);
Assert.Equal(1u, level1View.LevelCount);
var level0 = AcquireTexture(presenter, Storage(0));
Assert.Equal(0u, ((ImageRequest)GetFieldValue(level0, "Request")).View.BaseLevel);
Assert.NotEqual(((ImageView)GetFieldValue(level0, "View")).Handle, ((ImageView)GetFieldValue(level1, "View")).Handle);
var stacked = AcquireTexture(presenter, Storage(1, baseLevel: 1));
Assert.Equal(2u, ((ImageRequest)GetFieldValue(stacked, "Request")).View.BaseLevel);
using var fatal = new FatalScope();
Assert.Throws<SchedulerFatalException>(() => AcquireTexture(presenter, Storage(3)));
Assert.Contains("mip=3 levels=3", Assert.Single(fatal.Messages));
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void AcquisitionThatEndsTheTick_RecordsIntoTheNewBuffer()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
const uint width = 2048;
const uint height = 1024;
const ulong uploadBytes = width * height * 4;
var address = harness.MapBacked(uploadBytes, ReadWrite);
presenter.Run(() =>
{
var attachment = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address, width, height), false, false)!;
// Leave less than the upload at the end of the staging ring, owned by the current tick.
var staging = harness.Cache.GetUtilityBuffer(GpuBufferUsage.Upload);
Assert.True(staging.TryMap(staging.Size - uploadBytes / 2, out _));
staging.Commit();
presenter.InvokeMethod("BeginBatchedGuestCommands");
var tickBefore = harness.Scheduler.CurrentTick;
var bufferBefore = harness.Scheduler.Current.Handle;
presenter.InvokeMethod("AcquireColorAttachment", attachment);
Assert.True(harness.Scheduler.CurrentTick > tickBefore);
Assert.NotEqual(bufferBefore, harness.Scheduler.Current.Handle);
Assert.True((bool)GetFieldValue(presenter.Instance, "_batchOpen"));
Assert.Equal(harness.Scheduler.Current.Handle, ((CommandBuffer)presenter.InvokeMethod("BeginBatchedGuestCommands")!).Handle);
Assert.Equal(ImageLayout.ColorAttachmentOptimal, (ImageLayout)GetFieldValue(attachment, "Layout"));
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void LayeredAttachmentClear_ClearsEveryLayer()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
const int sliceBytes = 64 * 64 * 4;
var address = harness.MapBacked(0x10000, ReadWrite);
var pattern = new byte[2 * sliceBytes];
Array.Fill(pattern, (byte)0xAB);
harness.Write(address, pattern);
CachedImage image = null!;
presenter.Run(() =>
{
var attachment = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address, sliceMax: 1), false, false)!;
presenter.InvokeMethod("AcquireColorAttachment", attachment);
image = harness.Image((ResourceSlotIdentifier)GetFieldValue(attachment, "ImageIdentifier"));
Assert.Equal(2u, image.Backing.Layers);
var extent = new Extent2D(64, 64);
ClearColorAttachment(presenter, attachment, extent, 2, default);
presenter.InvokeMethod("ResetImageBindings");
harness.Scheduler.Finish();
});
var bytes = harness.ReadImageBytes(image);
Assert.Equal(2 * sliceBytes, bytes.Length);
Assert.All(bytes, value => Assert.Equal(0, value));
harness.Shutdown();
}
[Fact]
public void ColdScheduler_StartsRecordingBeforeTheFirstTargetLookup()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan, startScheduler: false);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
Assert.False(harness.Scheduler.Active);
presenter.Run(() =>
{
var attachment = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address), false, false);
Assert.NotNull(attachment);
Assert.True(harness.Scheduler.Active);
presenter.InvokeMethod("AcquireColorAttachment", attachment);
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(attachment, "View")).Handle);
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void ColdScheduler_StartsRecordingBeforeTheFirstTextureLookup()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan, startScheduler: false);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
Assert.False(harness.Scheduler.Active);
presenter.Run(() =>
{
var bindings = (Array)presenter.InvokeMethod("ResolveDrawTextures", new List<GuestDrawTexture> { Texture(address) })!;
Assert.True(harness.Scheduler.Active);
Assert.Equal(ImageLayout.ShaderReadOnlyOptimal, (ImageLayout)GetFieldValue(bindings.GetValue(0)!, "Layout"));
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void ConsecutiveLayerViews_UseDistinctAttachmentViews()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
presenter.Run(() =>
{
ImageView Bind(uint layer)
{
var attachment = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address, sliceMax: layer, sliceStart: layer), false, false)!;
presenter.InvokeMethod("AcquireColorAttachment", attachment);
var view = (ImageView)GetFieldValue(attachment, "View");
presenter.InvokeMethod("ResetImageBindings");
return view;
}
var layer1 = Bind(1);
var layer0 = Bind(0);
var layer0Again = Bind(0);
Assert.NotEqual(layer1.Handle, layer0.Handle);
Assert.Equal(layer0.Handle, layer0Again.Handle);
});
harness.Finish();
harness.Shutdown();
}
[Fact]
public void ShutdownScheduler_ShutsTheImageStoreDownBeforeTheBufferStoreAndTheScheduler()
{
if (!GatePrerequisites.Ready(_vulkan)) return;
using var presenter = new PresenterUnderTest(_vulkan);
var harness = presenter.Harness;
var address = harness.MapBacked(0x10000, ReadWrite);
presenter.Run(() =>
{
var attachment = presenter.InvokeMethod("DiscoverColorTarget", ColorTarget(address), false, false)!;
presenter.InvokeMethod("AcquireColorAttachment", attachment);
presenter.InvokeMethod("ResetImageBindings");
});
harness.Finish();
Assert.Single(harness.ImagesInRange(address, 4));
Assert.Equal(HostPageProtection.ReadOnly, harness.Protection(address));
presenter.Run(() => presenter.InvokeMethod("ShutdownScheduler"));
harness.MarkShutDown();
Assert.Empty(harness.ImagesInRange(address, 4));
Assert.Equal(HostPageProtection.ReadWrite, harness.Protection(address));
harness.Vulkan.AssertNoValidationMessages();
}
}