mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-16 04:11:51 +00:00
SharpEmu.Tools.GpuConformance executes the exec-cs.spv blob produced by SharpEmu.Tools.ShaderDump on a real Vulkan device (preferring a discrete GPU) and compares every word of the 64-byte storage buffer against CPU-computed expectations, bit for bit. Creating the compute pipeline doubles as a driver-acceptance check for SharpEmu's emitted SPIR-V. The checks cover the three ALU results, the store attempted with EXEC=0 (its destination must keep the sentinel), the store after EXEC is restored, and all trailing sentinel words. Any mismatch counts toward the failure total and makes the tool exit non-zero. Verified on an RTX 3060 Laptop GPU (NVIDIA) with all values matching, and the failure path verified to exit 1 by running a non-storing blob. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
413 lines
14 KiB
C#
413 lines
14 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
// Executes the SharpEmu-emitted "exec" conformance shader on a real Vulkan
|
|
// device and compares the buffer results against CPU-computed expected values.
|
|
//
|
|
// The shader (exec-cs.spv, produced by SharpEmu.Tools.ShaderDump) was
|
|
// translated by SharpEmu from hand-assembled Gen5 instruction words and stores
|
|
// results to guestBuffers[0]:
|
|
// [0] v_fmac_f32 -> fma(1.5f, 2.25f, 10.0f)
|
|
// [1] v_mul_hi_i32 -> high 32 bits of (int)0x7FFFFFFF * (int)0x00010003
|
|
// [2] v_mul_lo_i32 -> low 32 bits of the same product
|
|
// [3] store attempted with EXEC=0 -> must NOT land (sentinel remains)
|
|
// [4] store after EXEC restored -> 1.5f (0x3FC00000)
|
|
// Every other word of the buffer must still hold the sentinel afterwards.
|
|
//
|
|
// Creating the compute pipeline doubles as a driver-acceptance check for the
|
|
// emitted SPIR-V; the dispatch then verifies the arithmetic numerically.
|
|
//
|
|
// Usage: SharpEmu.Tools.GpuConformance <path-to-exec-cs.spv>
|
|
|
|
using Silk.NET.Core.Native;
|
|
using Silk.NET.Vulkan;
|
|
|
|
const uint Sentinel = 0xCAFEBABE;
|
|
|
|
// Must match the 64-byte global-memory binding ShaderDump constructs for the
|
|
// exec program.
|
|
const ulong BufferSize = 64;
|
|
|
|
var expectedFma = BitConverter.SingleToUInt32Bits(
|
|
MathF.FusedMultiplyAdd(1.5f, 2.25f, 10.0f));
|
|
var product = (long)0x7FFFFFFF * 0x00010003;
|
|
var expectedHi = (uint)(product >> 32);
|
|
var expectedLo = (uint)product;
|
|
var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
|
|
|
|
unsafe
|
|
{
|
|
var spvPath = args.Length > 0
|
|
? args[0]
|
|
: throw new InvalidOperationException(
|
|
"usage: SharpEmu.Tools.GpuConformance <path-to-exec-cs.spv>");
|
|
var code = File.ReadAllBytes(spvPath);
|
|
|
|
var vk = Vk.GetApi();
|
|
|
|
var appName = (byte*)SilkMarshal.StringToPtr("SharpEmuGpuConformance");
|
|
var appInfo = new ApplicationInfo
|
|
{
|
|
SType = StructureType.ApplicationInfo,
|
|
PApplicationName = appName,
|
|
ApiVersion = Vk.Version13,
|
|
};
|
|
var instanceInfo = new InstanceCreateInfo
|
|
{
|
|
SType = StructureType.InstanceCreateInfo,
|
|
PApplicationInfo = &appInfo,
|
|
};
|
|
Check(vk.CreateInstance(in instanceInfo, null, out var instance), "vkCreateInstance");
|
|
|
|
uint deviceCount = 0;
|
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, null);
|
|
if (deviceCount == 0)
|
|
{
|
|
Console.WriteLine("no Vulkan devices found");
|
|
return;
|
|
}
|
|
|
|
var physicalDevices = new PhysicalDevice[deviceCount];
|
|
fixed (PhysicalDevice* pDevices = physicalDevices)
|
|
{
|
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, pDevices);
|
|
}
|
|
|
|
// Prefer the first discrete GPU; fall back to the first device.
|
|
var physical = physicalDevices[0];
|
|
foreach (var candidate in physicalDevices)
|
|
{
|
|
vk.GetPhysicalDeviceProperties(candidate, out var props);
|
|
if (props.DeviceType == PhysicalDeviceType.DiscreteGpu)
|
|
{
|
|
physical = candidate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
vk.GetPhysicalDeviceProperties(physical, out var chosenProps);
|
|
Console.WriteLine(
|
|
$"executing on: {SilkMarshal.PtrToString((nint)chosenProps.DeviceName)}");
|
|
|
|
uint familyCount = 0;
|
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, null);
|
|
var families = new QueueFamilyProperties[familyCount];
|
|
fixed (QueueFamilyProperties* pFamilies = families)
|
|
{
|
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, pFamilies);
|
|
}
|
|
|
|
uint? computeFamilyFound = null;
|
|
for (uint index = 0; index < familyCount; index++)
|
|
{
|
|
if (families[index].QueueFlags.HasFlag(QueueFlags.ComputeBit))
|
|
{
|
|
computeFamilyFound = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var computeFamily = computeFamilyFound
|
|
?? throw new InvalidOperationException("device has no compute-capable queue family");
|
|
|
|
// The emitted SPIR-V declares the Int64 capability.
|
|
vk.GetPhysicalDeviceFeatures(physical, out var supportedFeatures);
|
|
if (!supportedFeatures.ShaderInt64)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"device does not support shaderInt64, which the emitted SPIR-V requires");
|
|
}
|
|
|
|
var priority = 1f;
|
|
var queueInfo = new DeviceQueueCreateInfo
|
|
{
|
|
SType = StructureType.DeviceQueueCreateInfo,
|
|
QueueFamilyIndex = computeFamily,
|
|
QueueCount = 1,
|
|
PQueuePriorities = &priority,
|
|
};
|
|
var features = new PhysicalDeviceFeatures { ShaderInt64 = true };
|
|
var deviceInfo = new DeviceCreateInfo
|
|
{
|
|
SType = StructureType.DeviceCreateInfo,
|
|
QueueCreateInfoCount = 1,
|
|
PQueueCreateInfos = &queueInfo,
|
|
PEnabledFeatures = &features,
|
|
};
|
|
Check(vk.CreateDevice(physical, in deviceInfo, null, out var device), "vkCreateDevice");
|
|
vk.GetDeviceQueue(device, computeFamily, 0, out var queue);
|
|
|
|
// Storage buffer, host-visible so the CPU can prefill and read back.
|
|
var bufferInfo = new BufferCreateInfo
|
|
{
|
|
SType = StructureType.BufferCreateInfo,
|
|
Size = BufferSize,
|
|
Usage = BufferUsageFlags.StorageBufferBit,
|
|
SharingMode = SharingMode.Exclusive,
|
|
};
|
|
Check(vk.CreateBuffer(device, in bufferInfo, null, out var buffer), "vkCreateBuffer");
|
|
vk.GetBufferMemoryRequirements(device, buffer, out var requirements);
|
|
vk.GetPhysicalDeviceMemoryProperties(physical, out var memoryProperties);
|
|
|
|
uint memoryType = uint.MaxValue;
|
|
for (var index = 0; index < memoryProperties.MemoryTypeCount; index++)
|
|
{
|
|
var flags = memoryProperties.MemoryTypes[index].PropertyFlags;
|
|
if ((requirements.MemoryTypeBits & (1u << index)) != 0 &&
|
|
flags.HasFlag(MemoryPropertyFlags.HostVisibleBit) &&
|
|
flags.HasFlag(MemoryPropertyFlags.HostCoherentBit))
|
|
{
|
|
memoryType = (uint)index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (memoryType == uint.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"no host-visible, host-coherent memory type available for the readback buffer");
|
|
}
|
|
|
|
var allocateInfo = new MemoryAllocateInfo
|
|
{
|
|
SType = StructureType.MemoryAllocateInfo,
|
|
AllocationSize = requirements.Size,
|
|
MemoryTypeIndex = memoryType,
|
|
};
|
|
Check(vk.AllocateMemory(device, in allocateInfo, null, out var memory), "vkAllocateMemory");
|
|
Check(vk.BindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory");
|
|
|
|
void* mapped;
|
|
Check(vk.MapMemory(device, memory, 0, BufferSize, 0, &mapped), "vkMapMemory");
|
|
var words = (uint*)mapped;
|
|
for (var index = 0; index < (int)(BufferSize / sizeof(uint)); index++)
|
|
{
|
|
words[index] = Sentinel;
|
|
}
|
|
|
|
// SharpEmu emits all guest buffers as one descriptor array at set 0,
|
|
// binding 0; this conformance shader uses a single buffer.
|
|
ShaderModule module;
|
|
fixed (byte* pCode = code)
|
|
{
|
|
var moduleInfo = new ShaderModuleCreateInfo
|
|
{
|
|
SType = StructureType.ShaderModuleCreateInfo,
|
|
CodeSize = (nuint)code.Length,
|
|
PCode = (uint*)pCode,
|
|
};
|
|
Check(vk.CreateShaderModule(device, in moduleInfo, null, out module), "vkCreateShaderModule");
|
|
}
|
|
|
|
var layoutBinding = new DescriptorSetLayoutBinding
|
|
{
|
|
Binding = 0,
|
|
DescriptorType = DescriptorType.StorageBuffer,
|
|
DescriptorCount = 1,
|
|
StageFlags = ShaderStageFlags.ComputeBit,
|
|
};
|
|
var setLayoutInfo = new DescriptorSetLayoutCreateInfo
|
|
{
|
|
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
|
BindingCount = 1,
|
|
PBindings = &layoutBinding,
|
|
};
|
|
Check(
|
|
vk.CreateDescriptorSetLayout(device, in setLayoutInfo, null, out var setLayout),
|
|
"vkCreateDescriptorSetLayout");
|
|
|
|
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
|
{
|
|
SType = StructureType.PipelineLayoutCreateInfo,
|
|
SetLayoutCount = 1,
|
|
PSetLayouts = &setLayout,
|
|
};
|
|
Check(
|
|
vk.CreatePipelineLayout(device, in pipelineLayoutInfo, null, out var pipelineLayout),
|
|
"vkCreatePipelineLayout");
|
|
|
|
var entryName = (byte*)SilkMarshal.StringToPtr("main");
|
|
var pipelineInfo = new ComputePipelineCreateInfo
|
|
{
|
|
SType = StructureType.ComputePipelineCreateInfo,
|
|
Stage = new PipelineShaderStageCreateInfo
|
|
{
|
|
SType = StructureType.PipelineShaderStageCreateInfo,
|
|
Stage = ShaderStageFlags.ComputeBit,
|
|
Module = module,
|
|
PName = entryName,
|
|
},
|
|
Layout = pipelineLayout,
|
|
};
|
|
Check(
|
|
vk.CreateComputePipelines(device, default, 1, in pipelineInfo, null, out var pipeline),
|
|
"vkCreateComputePipelines");
|
|
Console.WriteLine("driver accepted the SPIR-V (pipeline created)");
|
|
|
|
var poolSize = new DescriptorPoolSize
|
|
{
|
|
Type = DescriptorType.StorageBuffer,
|
|
DescriptorCount = 1,
|
|
};
|
|
var poolInfo = new DescriptorPoolCreateInfo
|
|
{
|
|
SType = StructureType.DescriptorPoolCreateInfo,
|
|
MaxSets = 1,
|
|
PoolSizeCount = 1,
|
|
PPoolSizes = &poolSize,
|
|
};
|
|
Check(vk.CreateDescriptorPool(device, in poolInfo, null, out var pool), "vkCreateDescriptorPool");
|
|
|
|
var setAllocateInfo = new DescriptorSetAllocateInfo
|
|
{
|
|
SType = StructureType.DescriptorSetAllocateInfo,
|
|
DescriptorPool = pool,
|
|
DescriptorSetCount = 1,
|
|
PSetLayouts = &setLayout,
|
|
};
|
|
Check(vk.AllocateDescriptorSets(device, in setAllocateInfo, out var descriptorSet), "vkAllocateDescriptorSets");
|
|
|
|
var descriptorBuffer = new DescriptorBufferInfo
|
|
{
|
|
Buffer = buffer,
|
|
Offset = 0,
|
|
Range = BufferSize,
|
|
};
|
|
var write = new WriteDescriptorSet
|
|
{
|
|
SType = StructureType.WriteDescriptorSet,
|
|
DstSet = descriptorSet,
|
|
DstBinding = 0,
|
|
DstArrayElement = 0,
|
|
DescriptorCount = 1,
|
|
DescriptorType = DescriptorType.StorageBuffer,
|
|
PBufferInfo = &descriptorBuffer,
|
|
};
|
|
vk.UpdateDescriptorSets(device, 1, in write, 0, null);
|
|
|
|
var commandPoolInfo = new CommandPoolCreateInfo
|
|
{
|
|
SType = StructureType.CommandPoolCreateInfo,
|
|
QueueFamilyIndex = computeFamily,
|
|
};
|
|
Check(vk.CreateCommandPool(device, in commandPoolInfo, null, out var commandPool), "vkCreateCommandPool");
|
|
|
|
var commandBufferInfo = new CommandBufferAllocateInfo
|
|
{
|
|
SType = StructureType.CommandBufferAllocateInfo,
|
|
CommandPool = commandPool,
|
|
Level = CommandBufferLevel.Primary,
|
|
CommandBufferCount = 1,
|
|
};
|
|
Check(vk.AllocateCommandBuffers(device, in commandBufferInfo, out var commandBuffer), "vkAllocateCommandBuffers");
|
|
|
|
var beginInfo = new CommandBufferBeginInfo
|
|
{
|
|
SType = StructureType.CommandBufferBeginInfo,
|
|
};
|
|
Check(vk.BeginCommandBuffer(commandBuffer, in beginInfo), "vkBeginCommandBuffer");
|
|
vk.CmdBindPipeline(commandBuffer, PipelineBindPoint.Compute, pipeline);
|
|
vk.CmdBindDescriptorSets(
|
|
commandBuffer,
|
|
PipelineBindPoint.Compute,
|
|
pipelineLayout,
|
|
0,
|
|
1,
|
|
in descriptorSet,
|
|
0,
|
|
null);
|
|
vk.CmdDispatch(commandBuffer, 1, 1, 1);
|
|
var barrier = new MemoryBarrier
|
|
{
|
|
SType = StructureType.MemoryBarrier,
|
|
SrcAccessMask = AccessFlags.ShaderWriteBit,
|
|
DstAccessMask = AccessFlags.HostReadBit,
|
|
};
|
|
vk.CmdPipelineBarrier(
|
|
commandBuffer,
|
|
PipelineStageFlags.ComputeShaderBit,
|
|
PipelineStageFlags.HostBit,
|
|
0,
|
|
1,
|
|
in barrier,
|
|
0,
|
|
null,
|
|
0,
|
|
null);
|
|
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer");
|
|
|
|
var submitInfo = new SubmitInfo
|
|
{
|
|
SType = StructureType.SubmitInfo,
|
|
CommandBufferCount = 1,
|
|
PCommandBuffers = &commandBuffer,
|
|
};
|
|
Check(vk.QueueSubmit(queue, 1, in submitInfo, default), "vkQueueSubmit");
|
|
Check(vk.QueueWaitIdle(queue), "vkQueueWaitIdle");
|
|
|
|
var results = new (string Name, uint Actual, uint Expected)[]
|
|
{
|
|
("v_fmac_f32 fma(1.5, 2.25, 10.0)", words[0], expectedFma),
|
|
("v_mul_hi_i32 hi(0x7FFFFFFF*0x10003)", words[1], expectedHi),
|
|
("v_mul_lo_i32 lo(0x7FFFFFFF*0x10003)", words[2], expectedLo),
|
|
("exec=0 store suppressed (offset 12 sentinel)", words[3], Sentinel),
|
|
("store after exec restore (offset 16)", words[4], expectedRestored),
|
|
};
|
|
var failures = 0;
|
|
foreach (var (name, actual, expected) in results)
|
|
{
|
|
var status = actual == expected ? "PASS" : "FAIL";
|
|
if (actual != expected)
|
|
{
|
|
failures++;
|
|
}
|
|
|
|
Console.WriteLine($"{status} {name}: gpu=0x{actual:X8} expected=0x{expected:X8}");
|
|
}
|
|
|
|
var totalWords = (int)(BufferSize / sizeof(uint));
|
|
var trailingClobbered = 0;
|
|
for (var index = results.Length; index < totalWords; index++)
|
|
{
|
|
if (words[index] != Sentinel)
|
|
{
|
|
trailingClobbered++;
|
|
Console.WriteLine(
|
|
$"FAIL trailing word [{index}] clobbered: gpu=0x{words[index]:X8} expected=0x{Sentinel:X8}");
|
|
}
|
|
}
|
|
|
|
failures += trailingClobbered;
|
|
if (trailingClobbered == 0)
|
|
{
|
|
Console.WriteLine(
|
|
$"PASS trailing words [{results.Length}..{totalWords - 1}] intact (sentinel)");
|
|
}
|
|
|
|
Console.WriteLine(failures == 0
|
|
? "RESULT: all values match"
|
|
: $"RESULT: {failures} mismatch(es)");
|
|
|
|
vk.DestroyCommandPool(device, commandPool, null);
|
|
vk.DestroyDescriptorPool(device, pool, null);
|
|
vk.DestroyPipeline(device, pipeline, null);
|
|
vk.DestroyPipelineLayout(device, pipelineLayout, null);
|
|
vk.DestroyDescriptorSetLayout(device, setLayout, null);
|
|
vk.DestroyShaderModule(device, module, null);
|
|
vk.UnmapMemory(device, memory);
|
|
vk.FreeMemory(device, memory, null);
|
|
vk.DestroyBuffer(device, buffer, null);
|
|
vk.DestroyDevice(device, null);
|
|
vk.DestroyInstance(instance, null);
|
|
|
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
|
|
|
static void Check(Result result, string what)
|
|
{
|
|
if (result != Result.Success)
|
|
{
|
|
throw new InvalidOperationException($"{what} failed: {result}");
|
|
}
|
|
}
|
|
}
|