mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-15 22:22:41 +00:00
[Tools] Add GPU conformance executor for dumped shader blobs (#127)
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>
This commit is contained in:
412
tools/SharpEmu.Tools.GpuConformance/Program.cs
Normal file
412
tools/SharpEmu.Tools.GpuConformance/Program.cs
Normal file
@@ -0,0 +1,412 @@
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- Standalone dev tool: opt out of the repo-wide lock-file requirement
|
||||
so no packages.lock.json is generated or committed for it. -->
|
||||
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user