mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
472fc96a37
The VOP3P emitter rejected any packed op with the clamp bit set. Clamp saturates each f16 output half to [0, 1] (and flushes NaN to 0, matching RDNA), so games that emit clamped packed arithmetic fell back to a loud emit failure. Apply the saturation to the f32 result of each lane, before it is narrowed back to f16. Because 0.0 and 1.0 are exact in both f32 and f16 and the clamp is monotonic, clamping in f32 and then rounding to f16 yields the same value as clamping the f16 result directly; for the fused multiply-add the pre-narrowing value is the round-to-odd f32, which preserves that equivalence through the final round-to-nearest-even. The saturation uses ordered compares so a NaN result collapses to 0 without a separate IsNan test. Verification: - The local exact-reference harness now also clamps: add, mul, and fma each compared against an f16-domain clamp reference (NaN -> 0, else [0, 1]) over directed boundary inputs and 24M random cases. 0 mismatches, alongside the existing 34M unclamped fma cases. - ShaderDump pk-f16 gains a clamped add and a clamped fma; all decode and emit. - The exec program computes the pinned fma with clamp (both lanes exceed 1.0, so each saturates to 0x3C00) and stores it at offset 28; GpuConformance checks it on device. All values match on an AMD Radeon RX 7700 XT.
432 lines
15 KiB
C#
432 lines
15 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)
|
|
// [5] v_pk_fma_f16 fma(2.5h, 21024h, 7.496e-5h) -> 0x7A6B packed; the exact
|
|
// sum sits just above an f16 midpoint, so a double-rounded f32
|
|
// multiply-add would give 0x7A6A instead
|
|
// [6] the same fma with the addend negated -> 0x7A6A packed (just below the
|
|
// same midpoint), pinning the opposite rounding direction
|
|
// [7] the pinned fma with the clamp modifier -> 0x3C00 packed (both lanes
|
|
// exceed 1.0, so each saturates to 1.0)
|
|
// 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);
|
|
|
|
// v_pk_fma_f16 of (0x4100, 0x7522, 0x04EA) per lane: the exact product
|
|
// 2.5 * 21024 = 52560 is an f16 tie (between 0x7A6A and 0x7A6B), so the tiny
|
|
// addend decides the rounding direction under a single fused rounding.
|
|
const uint ExpectedPkFma = 0x7A6B_7A6B;
|
|
const uint ExpectedPkFmaNeg = 0x7A6A_7A6A;
|
|
// Both lanes of the pinned fma are ~52560, far above 1.0, so the clamp modifier
|
|
// saturates each to 1.0 (0x3C00 in f16).
|
|
const uint ExpectedPkFmaClamp = 0x3C00_3C00;
|
|
|
|
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),
|
|
("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma),
|
|
("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg),
|
|
("v_pk_fma_f16 clamp saturates to 1.0", words[7], ExpectedPkFmaClamp),
|
|
};
|
|
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}");
|
|
}
|
|
}
|
|
}
|