From 5e54250752c714cf034c350c8bc5a1ac72e78395 Mon Sep 17 00:00:00 2001 From: Deeptanshu Lal <143280861+Deeptanshuu@users.noreply.github.com> Date: Wed, 15 Jul 2026 03:56:18 +0530 Subject: [PATCH] [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 --- .../SharpEmu.Tools.GpuConformance/Program.cs | 412 ++++++++++++++++++ .../SharpEmu.Tools.GpuConformance.csproj | 20 + 2 files changed, 432 insertions(+) create mode 100644 tools/SharpEmu.Tools.GpuConformance/Program.cs create mode 100644 tools/SharpEmu.Tools.GpuConformance/SharpEmu.Tools.GpuConformance.csproj diff --git a/tools/SharpEmu.Tools.GpuConformance/Program.cs b/tools/SharpEmu.Tools.GpuConformance/Program.cs new file mode 100644 index 0000000..778982d --- /dev/null +++ b/tools/SharpEmu.Tools.GpuConformance/Program.cs @@ -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 + +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 "); + 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}"); + } + } +} diff --git a/tools/SharpEmu.Tools.GpuConformance/SharpEmu.Tools.GpuConformance.csproj b/tools/SharpEmu.Tools.GpuConformance/SharpEmu.Tools.GpuConformance.csproj new file mode 100644 index 0000000..7cb935d --- /dev/null +++ b/tools/SharpEmu.Tools.GpuConformance/SharpEmu.Tools.GpuConformance.csproj @@ -0,0 +1,20 @@ + + + + + + Exe + true + + false + + + + + + +