sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs

517 lines
26 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Tests.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
using Xunit;
using Xunit.Abstractions;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
// Images and samplers bound through the class arrays of a compile request: element
// selection, the duplicated point sampler, and the per-mip storage descriptors.
public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestOutputHelper output) : IClassFixture<HeadlessVulkanFixture>
{
private const uint Format32Uint = 20;
private const uint Format32Sint = 21;
private const uint Format32Float = 22;
private const uint ImageType2D = 9;
private const uint ResultRegister = 8;
private const uint ResultBytes = 64;
private const uint FirstImageRegister = 16;
private const uint SecondImageRegister = 24;
private const uint SamplerRegister = 32;
private const ulong FirstImageAddress = 0x1000;
private const ulong SecondImageAddress = 0x2000;
private static IEnumerable<Gen5ShaderInstruction> ImageWords(ref uint pc, uint register, ulong address, uint format, uint lastLevel = 0)
{
uint[] words = [(uint)address, format << 20, 3 | (3 << 14), 0xFAC | (lastLevel << 16) | (ImageType2D << 28), 0, 0, 0, 0];
var instructions = new List<Gen5ShaderInstruction>();
for (uint dword = 0; dword < 8; dword++)
{
instructions.Add(MoveScalar(pc, register + dword, words[dword]));
pc += 8;
}
return instructions;
}
private static IEnumerable<Gen5ShaderInstruction> SamplerWords(ref uint pc, uint register)
{
var instructions = new List<Gen5ShaderInstruction>();
for (uint dword = 0; dword < 4; dword++)
{
instructions.Add(MoveScalar(pc, register + dword, 0));
pc += 8;
}
return instructions;
}
// Two images sampled at their centre through one sampler, each result stored to its own dword.
private static Gen5ShaderProgram SampleTwoImagesProgram(uint secondFormat)
{
var instructions = new List<Gen5ShaderInstruction>();
uint pc = 0;
instructions.AddRange(ImageWords(ref pc, FirstImageRegister, FirstImageAddress, Format32Float));
instructions.AddRange(ImageWords(ref pc, SecondImageRegister, SecondImageAddress, secondFormat));
instructions.AddRange(SamplerWords(ref pc, SamplerRegister));
instructions.Add(MoveVector(pc, 0, 0x3F00_0000)); pc += 8;
instructions.Add(MoveVector(pc, 1, 0x3F00_0000)); pc += 8;
instructions.Add(Image(pc, "ImageSampleLz", FirstImageRegister, SamplerRegister, dmask: 1)); pc += 8;
instructions.Add(BufferAccess(pc, "BufferStoreDword", ResultRegister, 0, 1, vectorData: 4)); pc += 8;
instructions.Add(Image(pc, "ImageSampleLz", SecondImageRegister, SamplerRegister, dmask: 1)); pc += 8;
instructions.Add(BufferAccess(pc, "BufferStoreDword", ResultRegister, 4, 1, vectorData: 4)); pc += 8;
instructions.Add(EndProgram(pc));
return Program([.. instructions]);
}
// Each lane writes its index into texel (0, 0) of the mip its index selects.
private static Gen5ShaderProgram StoreLaneIntoMipProgram()
{
var instructions = new List<Gen5ShaderInstruction>();
uint pc = 0;
instructions.AddRange(ImageWords(ref pc, FirstImageRegister, FirstImageAddress, Format32Uint, lastLevel: 1));
instructions.Add(MoveVector(pc, 1, 0)); pc += 8;
instructions.Add(MoveVector(pc, 2, 0)); pc += 8;
instructions.Add(Vop1(pc, "VMovB32", 3, Gen5Operand.Vector(0))); pc += 8;
instructions.Add(Vop1(pc, "VMovB32", 4, Gen5Operand.Vector(0))); pc += 8;
instructions.Add(Image(pc, "ImageStoreMip", FirstImageRegister, vectorAddress: 1, dmask: 1)); pc += 8;
instructions.Add(EndProgram(pc));
return Program([.. instructions]);
}
private static ImageDescription Describe(ulong guestOffset, Format format, GuestPixelFormat guestFormat, uint width, uint height, uint levels)
{
var description = ImageDescription.Create();
description.Data = new GuestSpan(ArrayBackedSpace.Base + guestOffset, (ulong)width * height * 4 * 2);
description.PixelFormat = format;
description.GuestFormat = guestFormat;
description.Extent = new Extent3D(width, height, 1);
description.Resources = new SubresourceCount(levels, 1);
description.Pitch = width;
description.BytesPerBlock = 4;
return description;
}
private sealed class Run : IDisposable
{
public Run(HeadlessVulkan vulkan, Gen5ShaderProgram program, uint[] registers, uint threadCount)
{
Plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, 0x5348_4152_5045_4D55, 0, 64);
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(Plan, Inputs(registers), ref snapshot, ref specialization));
Snapshot = snapshot;
Resources = ResourceMaterializer.ApplyTo(Plan, specialization);
var layout = BindingLayout.Allocate(
Resources.Info,
BindingLayout.CollectUserDataRegisters(program, 0, 64),
false,
ShaderCompileRequest.RequiresFlattenedTable(Plan, Resources),
false);
Request = new ShaderCompileRequest(Plan, Resources, layout) { LocalSizeX = threadCount, ThreadCountX = threadCount };
Assert.True(Gen5SpirvTranslator.TryCompileProgram(Request, out var shader, out var error), error);
Harness = new ImageTestHarness(vulkan);
Runner = new LayoutComputeRunner(Harness, Request, shader.Spirv);
Result = Runner.CreateBuffer(ResultBytes);
Registers = registers;
}
public ShaderResourcePlan Plan { get; }
public ResourceSnapshot Snapshot { get; }
public SpecializedResourceInfo Resources { get; }
public ShaderCompileRequest Request { get; }
public ImageTestHarness Harness { get; }
public LayoutComputeRunner Runner { get; }
public GpuBuffer Result { get; }
public uint[] Registers { get; }
// The image resource whose materialised descriptor starts at a guest address.
public int ResourceAt(ulong address)
{
for (var index = 0; index < Snapshot.Images.Length; index++)
{
if (Snapshot.Images[index][0] == (uint)address)
{
return index;
}
}
throw new InvalidOperationException($"no image descriptor at 0x{address:X}");
}
// The class-array entries in layout order, each resource mapped to its view info.
public Dictionary<DescriptorBindingKind, DescriptorImageInfo[]> BindImages(IReadOnlyDictionary<int, DescriptorImageInfo[]> viewsByResource, Sampler[]? samplers = null)
{
var bound = new Dictionary<DescriptorBindingKind, DescriptorImageInfo[]>();
foreach (var descriptor in Request.Bindings.Descriptors)
{
if (descriptor.Kind == DescriptorBindingKind.Samplers)
{
bound[descriptor.Kind] = descriptor.Resources.Select(index => new DescriptorImageInfo { Sampler = samplers![index] }).ToArray();
continue;
}
if (ImageDescriptorBinding.ResourceClass(descriptor.Kind) == ImageResourceClass.None)
{
continue;
}
var infos = new List<DescriptorImageInfo>();
var mipByResource = new Dictionary<uint, int>();
foreach (var resource in descriptor.Resources)
{
mipByResource.TryGetValue(resource, out var mip);
infos.Add(viewsByResource[(int)resource][mip]);
mipByResource[resource] = mip + 1;
}
bound[descriptor.Kind] = infos.ToArray();
}
return bound;
}
public void Dispatch(Dictionary<DescriptorBindingKind, DescriptorImageInfo[]> images, Action<CommandBuffer> prepare) =>
Harness.Run(() =>
{
prepare(new CommandBuffer(Harness.Scheduler.Current.Handle));
Runner.Dispatch(Registers, new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [Result] }, 1, boundImages: images);
});
public uint ResultWord(int index) => BinaryPrimitives.ReadUInt32LittleEndian(Runner.ReadBack(Result, 0, ResultBytes).AsSpan(index * 4));
public void Dispose()
{
Runner.Dispose();
Harness.Dispose();
}
}
private static uint[] UserData()
{
var registers = new uint[256];
registers[ResultRegister + 2] = ResultBytes;
return registers;
}
private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1, bool arrayed = false) =>
new()
{
ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount, Type = arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D }),
ImageLayout = ImageLayout.ShaderReadOnlyOptimal,
};
private static DescriptorImageInfo StorageView(CachedImage image, uint mip) =>
new()
{
ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.StorageBit, BaseLevel = mip, LevelCount = 1 }),
ImageLayout = ImageLayout.General,
};
[Fact]
public void SampledClassArray_SamplesEachElementThroughTheLayout()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan))
{
return;
}
using var run = new Run(vulkan, SampleTwoImagesProgram(Format32Float), UserData(), 1);
var floatClass = ImageDescriptorBinding.ForImage(run.Resources.Info.Images[0])!.Value;
Assert.Equal(2, run.Request.Bindings.Find(floatClass)!.Resources.Count);
Assert.Single(run.Resources.Info.Samplers);
var first = run.Harness.CreateImage(Describe(0, Format.R32Sfloat, GuestPixelFormat.Bits32Float, 1, 1, 1));
var second = run.Harness.CreateImage(Describe(0x10000, Format.R32Sfloat, GuestPixelFormat.Bits32Float, 1, 1, 1));
run.Harness.UploadImage(first, MemoryMarshal.AsBytes<float>([0.25f]), ImageTestHarness.WholeImageCopies(first.Description, 0));
run.Harness.UploadImage(second, MemoryMarshal.AsBytes<float>([0.75f]), ImageTestHarness.WholeImageCopies(second.Description, 0));
var sampler = run.Runner.CreateSampler(Filter.Linear);
var views = new Dictionary<int, DescriptorImageInfo[]>
{
[run.ResourceAt(FirstImageAddress)] = [SampledView(first)],
[run.ResourceAt(SecondImageAddress)] = [SampledView(second)],
};
run.Dispatch(run.BindImages(views, [sampler]), command =>
{
first.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
second.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
});
Assert.Equal(BitConverter.SingleToUInt32Bits(0.25f), run.ResultWord(0));
Assert.Equal(BitConverter.SingleToUInt32Bits(0.75f), run.ResultWord(1));
run.Harness.AssertNoValidationMessages();
output.WriteLine($"Verified sampled class arrays on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
[Fact]
public void PointOnlyImage_SamplesThroughTheDuplicatedSampler()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan))
{
return;
}
using var run = new Run(vulkan, SampleTwoImagesProgram(Format32Sint), UserData(), 1);
var samplers = run.Resources.Info.Samplers;
Assert.Equal(2, samplers.Count);
Assert.Equal(1, samplers.Count(sampler => sampler.ForcePointFiltering));
// Between the two texels a linear sampler averages; the integer image needs its point duplicate.
var floatImage = run.Harness.CreateImage(Describe(0, Format.R32Sfloat, GuestPixelFormat.Bits32Float, 2, 1, 1));
var integerImage = run.Harness.CreateImage(Describe(0x10000, Format.R32Sint, GuestPixelFormat.Bits32SInt, 2, 1, 1));
run.Harness.UploadImage(floatImage, MemoryMarshal.AsBytes<float>([1.0f, 3.0f]), ImageTestHarness.WholeImageCopies(floatImage.Description, 0));
run.Harness.UploadImage(integerImage, MemoryMarshal.AsBytes<int>([5, 9]), ImageTestHarness.WholeImageCopies(integerImage.Description, 0));
var hostSamplers = samplers.Select(sampler => run.Runner.CreateSampler(sampler.ForcePointFiltering ? Filter.Nearest : Filter.Linear)).ToArray();
var views = new Dictionary<int, DescriptorImageInfo[]>
{
[run.ResourceAt(FirstImageAddress)] = [SampledView(floatImage)],
[run.ResourceAt(SecondImageAddress)] = [SampledView(integerImage)],
};
run.Dispatch(run.BindImages(views, hostSamplers), command =>
{
floatImage.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
integerImage.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
});
Assert.Equal(BitConverter.SingleToUInt32Bits(2.0f), run.ResultWord(0));
Assert.Equal(9u, run.ResultWord(1));
run.Harness.AssertNoValidationMessages();
output.WriteLine($"Verified the duplicated point sampler on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
// Image 0 is plain, image 1 the indirect root, image 2 its candidate for key 1; a mip load
// reads texel (0, 0) of mip 1 of two-level images.
[Theory]
[InlineData(false, false, 2.0f)]
[InlineData(true, false, 2.5f)]
[InlineData(false, true, 2.0f)]
[InlineData(true, true, 2.5f)]
[InlineData(false, false, 1.0f, 9u, 13u, 0u)]
[InlineData(false, false, 2.0f, 9u, 13u, 1u)]
[InlineData(false, false, 1.0f, 13u, 9u, 0u)]
[InlineData(false, false, 2.0f, 13u, 9u, 1u)]
public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected,
uint firstImageType = 9, uint secondImageType = 9, uint selectedRecord = 1)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan))
{
return;
}
var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector,
firstImageType, secondImageType, firstImageType == 13 || secondImageType == 13 ? 5u : 1u);
var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip, firstImageType, secondImageType);
registers[8] = selectedRecord;
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var result = runner.CreateBuffer(ResultBytes);
var material = runner.CreateBuffer(MemoryMarshal.AsBytes(memory.Words.AsSpan(0, 448 / 4)));
var buffers = snapshot.Buffers.Select(descriptor => descriptor[0] == 0x1000 ? material : result).ToArray();
// Two-level images hold their base value in mip 0 and that value plus one half in mip 1.
var levels = loadMip ? 2u : 1u;
var images = new CachedImage[3];
var infos = new Dictionary<int, DescriptorImageInfo[]>();
float[] texels = [0.5f, 1.0f, 2.0f];
for (var index = 0; index < 3; index++)
{
var description = Describe((ulong)index * 0x10000, Format.R32Sfloat, GuestPixelFormat.Bits32Float, levels, levels, levels);
images[index] = harness.CreateImage(description);
var data = Enumerable.Repeat(texels[index], (int)(levels * levels)).Concat(levels > 1 ? [texels[index] + 0.5f] : Array.Empty<float>()).ToArray();
harness.UploadImage(images[index], MemoryMarshal.AsBytes<float>(data), ImageTestHarness.WholeImageCopies(description, 0));
infos[index] = [SampledView(images[index], levels, request.Resources.Info.Images[index].Dimension == ImageDimension.Dim2DArray)];
}
Assert.Equal(0x20u, snapshot.Images[1][0]);
Assert.Equal(0x21u, snapshot.Images[2][0]);
var samplers = request.Resources.Info.Samplers.Select(_ => runner.CreateSampler(Filter.Nearest)).ToArray();
var bound = new Dictionary<DescriptorBindingKind, DescriptorImageInfo[]>();
foreach (var descriptor in request.Bindings.Descriptors)
{
if (descriptor.Kind == DescriptorBindingKind.Samplers)
{
bound[descriptor.Kind] = descriptor.Resources.Select(index => new DescriptorImageInfo { Sampler = samplers[index] }).ToArray();
}
else if (ImageDescriptorBinding.ResourceClass(descriptor.Kind) != ImageResourceClass.None)
{
bound[descriptor.Kind] = descriptor.Resources.Select(index => infos[(int)index][0]).ToArray();
}
}
harness.Run(() =>
{
var command = new CommandBuffer(harness.Scheduler.Current.Handle);
foreach (var image in images)
{
image.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
}
runner.Dispatch(
registers,
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = buffers },
1,
flattenedTable: snapshot.FlattenedResourceTable,
boundImages: bound);
});
var words = runner.ReadBack(result, 0, ResultBytes);
Assert.Equal(BitConverter.SingleToUInt32Bits(0.5f), BinaryPrimitives.ReadUInt32LittleEndian(words.AsSpan(0)));
Assert.Equal(BitConverter.SingleToUInt32Bits(expected), BinaryPrimitives.ReadUInt32LittleEndian(words.AsSpan(4)));
harness.AssertNoValidationMessages();
output.WriteLine($"Verified indirect image selection (loadMip={loadMip}) on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
[Theory]
[InlineData(0u, 22u, false)]
[InlineData(1u, 11u, false)]
[InlineData(2u, 22u, false)]
[InlineData(0x80000000u, 22u, false)]
[InlineData(0u, 22u, true)]
[InlineData(1u, 11u, true)]
[InlineData(2u, 22u, true)]
[InlineData(0x80000000u, 22u, true)]
[InlineData(0u, 0u, false, true)]
[InlineData(1u, 11u, false, true)]
[InlineData(2u, 22u, false, true)]
[InlineData(0x80000000u, 22u, false, true)]
[InlineData(0u, 0u, true, true)]
[InlineData(1u, 11u, true, true)]
[InlineData(2u, 22u, true, true)]
[InlineData(0x80000000u, 22u, true, true)]
public void DirectImageTableUsesCapturedOffset(uint mask, uint expected, bool split, bool guarded = false)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
var (_, snapshot, request) = DirectImageTableTests.PrepareDirect(mask, split, guarded);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var result = runner.CreateBuffer(new byte[ResultBytes]);
var images = new CachedImage[snapshot.Images.Length];
var views = new DescriptorImageInfo[images.Length];
for (var index = 0; index < images.Length; index++)
{
var description = Describe((ulong)index * 0x10000, Format.R32Uint, GuestPixelFormat.Bits32UInt, 1, 1, 1);
images[index] = harness.CreateImage(description);
uint[] texel = [snapshot.Images[index][0] == 0x1000 ? 11u : 22u];
harness.UploadImage(images[index], MemoryMarshal.AsBytes<uint>(texel), ImageTestHarness.WholeImageCopies(description, 0));
views[index] = SampledView(images[index]);
}
var bound = request.Bindings.Descriptors
.Where(binding => ImageDescriptorBinding.ResourceClass(binding.Kind) != ImageResourceClass.None)
.ToDictionary(binding => binding.Kind, binding => binding.Resources.Select(index => views[index]).ToArray());
harness.Run(() =>
{
var command = new CommandBuffer(harness.Scheduler.Current.Handle);
foreach (var image in images)
image.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
runner.Dispatch([0x1000, 0], new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] },
1, flattenedTable: snapshot.FlattenedResourceTable, boundImages: bound);
});
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(runner.ReadBack(result, 0, sizeof(uint))));
harness.AssertNoValidationMessages();
}
[Theory]
[InlineData(1u, true, 37u)]
[InlineData(0x80000000u, true, 91u)]
[InlineData(1u, false, 91u)]
[InlineData(0x80000000u, false, 37u)]
public void MixedImageDimensionsSelectTheirOwnBinding(uint mask, bool arrayFirst, uint expected)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
var (snapshot, request) = DirectImageTableTests.PrepareMixedDimensions(mask, arrayFirst);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var result = runner.CreateBuffer(new byte[ResultBytes]);
var images = new CachedImage[snapshot.Images.Length];
var views = new DescriptorImageInfo[images.Length];
for (var index = 0; index < images.Length; index++)
{
var array = request.Resources.Info.Images[index].Dimension == ImageDimension.Dim2DArray;
var description = Describe((ulong)index * 0x10000, Format.R32Uint, GuestPixelFormat.Bits32UInt, 1, 1, 1);
description.Resources = new SubresourceCount(1, array ? 2u : 1u);
images[index] = harness.CreateImage(description);
uint[] texels = array ? [13u, 37u] : [91u];
harness.UploadImage(images[index], MemoryMarshal.AsBytes<uint>(texels), ImageTestHarness.WholeImageCopies(description, 0));
views[index] = new DescriptorImageInfo
{
ImageView = images[index].GetOrCreateView(ImageViewDescription.Default with
{
Format = Format.R32Uint,
Type = array ? ImageViewType.Type2DArray : ImageViewType.Type2D,
LayerCount = description.Resources.Layers,
}),
ImageLayout = ImageLayout.ShaderReadOnlyOptimal,
};
}
var bound = request.Bindings.Descriptors
.Where(binding => ImageDescriptorBinding.ResourceClass(binding.Kind) != ImageResourceClass.None)
.ToDictionary(binding => binding.Kind, binding => binding.Resources.Select(index => views[index]).ToArray());
harness.Run(() =>
{
var command = new CommandBuffer(harness.Scheduler.Current.Handle);
foreach (var image in images)
image.Transition(ImageLayout.ShaderReadOnlyOptimal, AccessFlags.ShaderReadBit, null, command);
runner.Dispatch([0x1000, 0], new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] },
1, flattenedTable: snapshot.FlattenedResourceTable, boundImages: bound);
});
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(runner.ReadBack(result, 0, sizeof(uint))));
harness.AssertNoValidationMessages();
}
[Fact]
public void DynamicMipStorageImage_WritesEachMipThroughItsOwnDescriptor()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan))
{
return;
}
using var run = new Run(vulkan, StoreLaneIntoMipProgram(), UserData(), 2);
var image = run.Resources.Info.Images[0];
Assert.Equal(ImageMipMode.DynamicStorage, image.MipMode);
Assert.Equal(2u, image.MipCount);
var storageClass = ImageDescriptorBinding.ForImage(image)!.Value;
Assert.Equal(2, run.Request.Bindings.Find(storageClass)!.Resources.Count);
var description = Describe(0, Format.R32Uint, GuestPixelFormat.Bits32UInt, 2, 2, 2);
var target = run.Harness.CreateImage(description);
var copies = ImageTestHarness.WholeImageCopies(description, 0);
var texelCount = (int)(ImageTestHarness.WholeImageBytes(description) / 4);
var sentinel = Enumerable.Repeat(0xFFFF_FFFFu, texelCount).ToArray();
run.Harness.UploadImage(target, MemoryMarshal.AsBytes<uint>(sentinel), copies);
var views = new Dictionary<int, DescriptorImageInfo[]> { [0] = [StorageView(target, 0), StorageView(target, 1)] };
run.Dispatch(run.BindImages(views), command => target.Transition(ImageLayout.General, AccessFlags.ShaderWriteBit, null, command));
var texels = MemoryMarshal.Cast<byte, uint>(run.Harness.ReadImage(target, copies, (ulong)texelCount * 4)).ToArray();
Assert.Equal(0u, texels[0]);
Assert.Equal(sentinel[1], texels[1]);
Assert.Equal(sentinel[2], texels[2]);
Assert.Equal(sentinel[3], texels[3]);
Assert.Equal(1u, texels[4]);
run.Harness.AssertNoValidationMessages();
output.WriteLine($"Verified per-mip storage descriptors on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
}