fix(gen5): handle runtime and shader buffer paths (#904)

* Implement AMPR wait-on-address commands

* Accept Gen5 AJM initialization options

* Preserve opaque context register table values

* Support shader-loaded buffer descriptors

* Support V_CVT_U16_F16 shader instruction
This commit is contained in:
Mathias 2026-09-25 22:37:09 +01:00 committed by GitHub
parent 7dbdd8b2cc
commit 8feea60d31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 960 additions and 17 deletions

View File

@ -7,6 +7,7 @@ using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Ampr;
@ -35,6 +36,9 @@ public static class AmprExports
private const ulong AprMaxAppAddress = 0x0000f00000000000;
private const ulong KernelEventQueueRecordSize = 0x20;
private const ulong WriteAddressRecordSize = 0x20;
private const ulong WaitAddressRecordSizeWithoutReference = 0x08;
private const ulong WaitAddressRecordSizeWith32BitReference = 0x0C;
private const ulong WaitAddressRecordSizeWith64BitReference = 0x10;
private const uint ReadFileRecordType = 0x17;
private const uint KernelEventQueueRecordType = 2;
private const uint WriteAddressRecordType = 3;
@ -55,6 +59,7 @@ public static class AmprExports
public readonly List<ReadFileCommand> ReadFileCommands = [];
public readonly List<KernelEventCommand> KernelEventCommands = [];
public readonly List<WriteAddressCommand> WriteAddressCommands = [];
public readonly List<WaitAddressCommand> WaitAddressCommands = [];
public bool GatherScatterValid;
public uint GatherScatterFileId;
public ulong GatherScatterDestination;
@ -86,6 +91,15 @@ public static class AmprExports
public ulong Value;
}
private sealed class WaitAddressCommand
{
public ulong RecordOffset;
public ulong Address;
public ulong Reference;
public uint Compare;
public uint Flush;
}
private sealed class CachedHostFile : IDisposable
{
public CachedHostFile(string path)
@ -578,6 +592,37 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jIlc4p5dSD0",
ExportName = "sceAmprMeasureCommandSizeWaitOnAddress",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWaitOnAddress(CpuContext ctx) => MeasureCommandSizeWaitOnAddress(ctx, legacy: false);
[SysAbiExport(
Nid = "0BMj1hgG+kE",
ExportName = "sceAmprMeasureCommandSizeWaitOnAddress_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWaitOnAddress0400(CpuContext ctx) => MeasureCommandSizeWaitOnAddress(ctx, legacy: true);
private static int MeasureCommandSizeWaitOnAddress(CpuContext ctx, bool legacy)
{
var address = ctx[CpuRegister.Rdi];
var reference = ctx[CpuRegister.Rsi];
var compare = ctx[CpuRegister.Rdx];
var flush = ctx[CpuRegister.Rcx];
if (!IsValidWaitAddressParameters(address, compare, flush, legacy))
{
return SetAmprResult(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var recordSize = GetWaitAddressRecordSize(reference);
TraceAmpr(ctx, legacy ? "measure_wait_address_04_00" : "measure_wait_address", address, recordSize, reference);
ctx[CpuRegister.Rax] = recordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "tZDDEo2tE5k",
ExportName = "sceAmprCommandBufferGetSize",
@ -769,6 +814,42 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "V7GQTEeUfhw",
ExportName = "sceAmprCommandBufferWaitOnAddress",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWaitOnAddress(CpuContext ctx) => CommandBufferWaitOnAddress(ctx, legacy: false);
[SysAbiExport(
Nid = "DLfoNxTFNVk",
ExportName = "sceAmprCommandBufferWaitOnAddress_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWaitOnAddress0400(CpuContext ctx) => CommandBufferWaitOnAddress(ctx, legacy: true);
private static int CommandBufferWaitOnAddress(CpuContext ctx, bool legacy)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var address = ctx[CpuRegister.Rsi];
var reference = ctx[CpuRegister.Rdx];
var compare = ctx[CpuRegister.Rcx];
var flush = ctx[CpuRegister.R8];
if (commandBuffer == 0 || !IsValidWaitAddressParameters(address, compare, flush, legacy))
{
return SetAmprResult(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!AppendWaitAddressRecord(ctx, commandBuffer, address, reference, (uint)compare, (uint)flush))
{
return SetAmprResult(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAmpr(ctx, legacy ? "wait_address_04_00" : "wait_address", commandBuffer, address, reference);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
{
return CompleteCommandBuffer(ctx, commandBuffer, out _, out _);
@ -797,16 +878,18 @@ public static class AmprExports
ReadFileCommand[] readCommands;
KernelEventCommand[] kernelEventCommands;
WriteAddressCommand[] writeAddressCommands;
WaitAddressCommand[] waitAddressCommands;
lock (state)
{
writeOffset = state.WriteOffset;
readCommands = state.ReadFileCommands.ToArray();
kernelEventCommands = state.KernelEventCommands.ToArray();
writeAddressCommands = state.WriteAddressCommands.ToArray();
waitAddressCommands = state.WaitAddressCommands.ToArray();
}
var commands = new List<(ulong Offset, int Kind, int Index)>(
readCommands.Length + kernelEventCommands.Length + writeAddressCommands.Length);
readCommands.Length + kernelEventCommands.Length + writeAddressCommands.Length + waitAddressCommands.Length);
for (var i = 0; i < readCommands.Length; i++)
{
commands.Add((readCommands[i].RecordOffset, 0, i));
@ -819,6 +902,10 @@ public static class AmprExports
{
commands.Add((writeAddressCommands[i].RecordOffset, 2, i));
}
for (var i = 0; i < waitAddressCommands.Length; i++)
{
commands.Add((waitAddressCommands[i].RecordOffset, 3, i));
}
commands.Sort(static (left, right) => left.Offset.CompareTo(right.Offset));
foreach (var commandEntry in commands)
@ -860,6 +947,17 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
break;
case 3:
var waitCommand = waitAddressCommands[commandEntry.Index];
var waitResult = WaitForAddress(ctx, waitCommand);
if (waitResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
executionResult = waitResult;
errorOffset = checked((uint)commandEntry.Offset);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
break;
}
}
@ -957,6 +1055,7 @@ public static class AmprExports
state.ReadFileCommands.Clear();
state.KernelEventCommands.Clear();
state.WriteAddressCommands.Clear();
state.WaitAddressCommands.Clear();
state.GatherScatterValid = false;
state.GatherScatterFileId = 0;
state.GatherScatterDestination = 0;
@ -1023,6 +1122,7 @@ public static class AmprExports
state.ReadFileCommands.Clear();
state.KernelEventCommands.Clear();
state.WriteAddressCommands.Clear();
state.WaitAddressCommands.Clear();
state.GatherScatterValid = false;
state.GatherScatterFileId = 0;
state.GatherScatterDestination = 0;
@ -1044,7 +1144,8 @@ public static class AmprExports
{
return state.ReadFileCommands.Count != 0 ||
state.KernelEventCommands.Count != 0 ||
state.WriteAddressCommands.Count != 0;
state.WriteAddressCommands.Count != 0 ||
state.WaitAddressCommands.Count != 0;
}
}
@ -1515,6 +1616,139 @@ public static class AmprExports
return true;
}
private static bool AppendWaitAddressRecord(
CpuContext ctx,
ulong commandBuffer,
ulong address,
ulong reference,
uint compare,
uint flush)
{
var recordSize = GetWaitAddressRecordSize(reference);
Span<byte> record = stackalloc byte[(int)recordSize];
record.Clear();
var dwordCount = (uint)(recordSize / sizeof(uint));
var header = (uint)((address >> 16) & 0xFFFF0000UL);
header |= (((dwordCount << 8) + 0xF00u) & 0xEF00u) |
((compare & 7u) << 13) |
((flush & 1u) << 12) |
1u;
BinaryPrimitives.WriteUInt32LittleEndian(record, header);
BinaryPrimitives.WriteUInt32LittleEndian(record[sizeof(uint)..], (uint)address);
if (reference != 0)
{
BinaryPrimitives.WriteUInt32LittleEndian(record[(2 * sizeof(uint))..], (uint)reference);
if (dwordCount == 4)
{
BinaryPrimitives.WriteUInt32LittleEndian(record[(3 * sizeof(uint))..], (uint)(reference >> 32));
}
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
return false;
}
lock (state)
{
if (state.Buffer == 0 ||
state.WriteOffset > state.Size ||
recordSize > state.Size - state.WriteOffset ||
!ctx.Memory.TryWrite(state.Buffer + state.WriteOffset, record))
{
return false;
}
var recordOffset = state.WriteOffset;
state.WaitAddressCommands.Add(new WaitAddressCommand
{
RecordOffset = recordOffset,
Address = address,
Reference = reference,
Compare = compare,
Flush = flush,
});
state.WriteOffset += recordSize;
state.CommandCount++;
if (!WriteCommandBufferProgress(ctx, commandBuffer, state))
{
state.WaitAddressCommands.RemoveAt(state.WaitAddressCommands.Count - 1);
state.WriteOffset -= recordSize;
state.CommandCount--;
return false;
}
}
return true;
}
private static int WaitForAddress(CpuContext ctx, WaitAddressCommand command)
{
var spinWait = new SpinWait();
while (true)
{
if (command.Flush != 0)
{
Thread.MemoryBarrier();
}
if (!ctx.TryReadUInt64(command.Address, out var value))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (IsWaitConditionSatisfied(value, command.Reference, command.Compare))
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
spinWait.SpinOnce();
}
}
private static bool IsWaitConditionSatisfied(ulong value, ulong reference, uint compare) => compare switch
{
0 => value == reference,
1 => value > reference,
2 => value < reference,
3 => value != reference,
4 => unchecked((long)(value - reference)) >= 0,
5 => unchecked((long)value) > unchecked((long)reference),
6 => unchecked((long)value) < unchecked((long)reference),
_ => false,
};
private static bool IsValidWaitAddressParameters(ulong address, ulong compare, ulong flush, bool legacy)
{
var maxCompare = legacy ? 6UL : 3UL;
return (address & 7UL) == 0 &&
address <= AprMaxAppAddress &&
AprMaxAppAddress - address >= sizeof(ulong) &&
(!legacy || address != 0) &&
compare <= maxCompare &&
flush <= 1;
}
private static ulong GetWaitAddressRecordSize(ulong reference)
{
if (reference == 0)
{
return WaitAddressRecordSizeWithoutReference;
}
return (reference >> 32) == 0
? WaitAddressRecordSizeWith32BitReference
: WaitAddressRecordSizeWith64BitReference;
}
private static int SetAmprResult(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)(long)value);
return value;
}
private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan<byte> record)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)

View File

@ -96,9 +96,15 @@ public static class AjmExports
public static int AjmInitialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var reservedOrOptions = ctx[CpuRegister.Rdi];
var outputAddress = ctx[CpuRegister.Rsi];
if (reserved != 0 || outputAddress == 0)
// The Gen4 AJM ABI requires a zero reserved argument. Gen5 callers
// also use this slot for an opaque initialization value (observed as
// 0x0000000300000000 in a retail title), which the software backend
// does not need to interpret. Keep the output pointer validation for
// both generations; only relax the legacy reserved-field check on
// Gen5.
if ((ctx.TargetGeneration == Generation.Gen4 && reservedOrOptions != 0) || outputAddress == 0)
{
return unchecked((int)0x806A0001);
}
@ -115,7 +121,8 @@ public static class AjmExports
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
$"[LOADER][TRACE] ajm.initialize reserved_or_options=0x{reservedOrOptions:X16} " +
$"generation={ctx.TargetGeneration} out=0x{outputAddress:X16} context={contextId}");
}
ctx[CpuRegister.Rax] = 0;

View File

@ -375,6 +375,8 @@ public sealed class ContextRegisters
// The two reserved registers behind the depth block, written by the composite depth binding.
public uint ReservedDepthRegister1;
public uint ReservedDepthRegister3;
// Preserve context-table values for in-bank registers without a decoded renderer field.
public Dictionary<uint, uint> UnmodeledTableRegisters = new();
public float DepthClearValue;
public float DepthBoundsMin;
public float DepthBoundsMax = 1f;
@ -397,6 +399,7 @@ public sealed class ContextRegisters
copy.ScreenViewport = ScreenViewport.Copy();
copy.SampleLocations = SampleLocations.Copy();
copy.ShaderInterface = ShaderInterface.Copy();
copy.UnmodeledTableRegisters = new Dictionary<uint, uint>(UnmodeledTableRegisters);
return copy;
}

View File

@ -57,7 +57,16 @@ public static class RegisterWriteTable
public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
{
WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress);
if (offset < ContextIndirect.Length && ContextIndirect[offset] is null)
{
// Keep unknown in-bank table values so a later renderer implementation can decode them.
banks.Context.UnmodeledTableRegisters[offset] = value;
}
else
{
WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress);
}
if (Rendering.RenderTrace.Enabled)
{
TraceDepthStateWrite(offset, value, tableAddress, "table");

View File

@ -239,6 +239,8 @@ public static partial class Gen5MslTranslator
"VCvtF32I32" => FloatResult(instruction, $"(float)as_type<int>({RawSource(instruction, 0)})"),
"VCvtF32U32" => FloatResult(instruction, $"(float)({RawSource(instruction, 0)})"),
"VCvtU32F32" => $"(uint)({F(instruction, 0)})",
"VCvtU16F16" =>
$"(uint)clamp(trunc(isnan({F16(instruction, 0)}) ? 0.0f : {F16(instruction, 0)}), 0.0f, 65535.0f)",
"VCvtI32F32" => AsUInt($"(int)({F(instruction, 0)})"),
// RPI rounds toward positive infinity; FLR toward negative.
"VCvtRpiI32F32" => AsUInt($"(int)ceil({F(instruction, 0)})"),

View File

@ -152,6 +152,25 @@ public static partial class Gen5SpirvTranslator
_uintType,
GetFloatSource(instruction, 0));
break;
case "VCvtU16F16":
{
var source = GetFloat16Source(instruction, 0);
var sourceIsNan = _module.AddInstruction(
SpirvOp.IsNan,
_boolType,
source);
source = _module.AddInstruction(
SpirvOp.Select,
_floatType,
sourceIsNan,
Float(0),
source);
var bounded = Ext(43, _floatType, source, Float(0), Float(65535));
result = BitwiseAnd(
_module.AddInstruction(SpirvOp.ConvertFToU, _uintType, bounded),
UInt(0xFFFF));
break;
}
case "VCvtI32F32":
case "VCvtRpiI32F32":
case "VCvtFlrI32F32":

View File

@ -664,7 +664,7 @@ public static partial class Gen5SpirvTranslator
// One dword of a scalar buffer load through the V# in s[descriptor:descriptor+3]:
// base in dwords 0-1 (48 bits), stride in dword 1 [29:16], records in dword 2.
// A load past the end of the buffer returns zero, as on hardware.
private uint LoadDeviceDescriptorBufferWord(uint descriptor, uint byteOffset)
private (uint BaseAddress, uint Size, uint Stride, uint Word3) LoadDeviceBufferDescriptor(uint descriptor)
{
var word1 = LoadS(descriptor + 1);
var baseAddress = Pair64(LoadS(descriptor), BitwiseAnd(word1, UInt(0xFFFF)));
@ -676,14 +676,52 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
records,
_module.AddInstruction(SpirvOp.IMul, _ulongType, records, Widen(stride)));
var aligned = Widen(BitwiseAnd(byteOffset, UInt(~3u)));
var inRange = ULessThan64(IAdd64(aligned, ULong(sizeof(uint) - 1)), size);
return (baseAddress, size, stride, LoadS(descriptor + 3));
}
private uint IsDeviceBufferByteRangeInRange(uint size64, uint byteOffset, uint byteCount) =>
ULessThan64(
IAdd64(Widen(byteOffset), ULong((ulong)byteCount - 1)),
size64);
private uint IsDeviceBufferElementInRange(uint size64, uint byteOffset, uint lastByteOffset) =>
ULessThan64(
IAdd64(Widen(byteOffset), Widen(lastByteOffset)),
size64);
private uint LoadDeviceBufferWord(uint baseAddress, uint size64, uint byteOffset)
=> LoadDeviceBufferWord(baseAddress, size64, byteOffset, _module.ConstantBool(true));
private uint LoadDeviceBufferWord(uint baseAddress, uint size64, uint byteOffset, uint accessAllowed)
{
var alignedOffset = BitwiseAnd(byteOffset, UInt(~3u));
var inRange = LogicalAnd(
accessAllowed,
IsDeviceBufferByteRangeInRange(size64, alignedOffset, sizeof(uint)));
var address = And64(
IAdd64(baseAddress, Widen(alignedOffset)),
ULong(DeviceAddressMask & ~3ul));
Store(_deviceBufferWordScratch, UInt(0));
EmitConditional(inRange, () =>
Store(_deviceBufferWordScratch, LoadDeviceDword(And64(IAdd64(baseAddress, aligned), ULong(DeviceAddressMask & ~3ul)))));
EmitConditional(inRange, () => Store(_deviceBufferWordScratch, LoadDeviceDword(address)));
return Load(_uintType, _deviceBufferWordScratch);
}
private void StoreDeviceBufferWord(uint baseAddress, uint size64, uint byteOffset, uint value)
{
var alignedOffset = BitwiseAnd(byteOffset, UInt(~3u));
var inRange = IsDeviceBufferByteRangeInRange(size64, alignedOffset, sizeof(uint));
var address = And64(
IAdd64(baseAddress, Widen(alignedOffset)),
ULong(DeviceAddressMask & ~3ul));
EmitConditional(inRange, () => StoreDeviceDword(address, value, _module.ConstantBool(true)));
}
private uint LoadDeviceDescriptorBufferWord(uint descriptor, uint byteOffset)
{
var (baseAddress, size, _, _) = LoadDeviceBufferDescriptor(descriptor);
return LoadDeviceBufferWord(baseAddress, size, byteOffset);
}
private bool TryEmitLayoutScalarMemory(Gen5ShaderInstruction instruction, Gen5ScalarMemoryControl control, out string error)
{
_deviceAddressInstructionPc = instruction.Pc;

View File

@ -2220,6 +2220,11 @@ public static partial class Gen5SpirvTranslator
return TryEmitVertexInputFetch(control, vertexInput, out error);
}
if (_request.Memory.Find(instruction.Pc) is { DeviceDescriptor: true })
{
return TryEmitDeviceDescriptorBufferMemory(instruction, control, out error);
}
int bindingIndex;
uint stride;
uint descriptorWord3;
@ -2476,6 +2481,351 @@ public static partial class Gen5SpirvTranslator
return true;
}
private bool TryEmitDeviceDescriptorBufferMemory(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
out string error)
{
error = string.Empty;
if (control.Typed || instruction.Opcode.StartsWith("TBuffer", StringComparison.Ordinal))
{
error = $"device buffer descriptor operation {instruction.Opcode} is not supported";
return false;
}
_deviceAddressInstructionPc = instruction.Pc;
var (baseAddress, size, stride, descriptorWord3) = LoadDeviceBufferDescriptor(control.ScalarResource);
var scalarOffset = instruction.Sources.Count > 2
? GetRawSource(instruction, 2)
: UInt(0);
var vectorIndex = control.IndexEnabled
? LoadV(control.VectorAddress)
: UInt(0);
var vectorOffset = control.OffsetEnabled
? LoadV(control.VectorAddress + (control.IndexEnabled ? 1u : 0u))
: UInt(0);
var byteAddress = IAdd(UInt(unchecked((uint)control.OffsetBytes)), scalarOffset);
byteAddress = IAdd(byteAddress, vectorOffset);
byteAddress = IAdd(byteAddress, _module.AddInstruction(SpirvOp.IMul, _uintType, vectorIndex, stride));
if (IsFormatBufferLoad(instruction.Opcode))
{
EmitDeviceBufferFormatLoad(
baseAddress,
size,
byteAddress,
descriptorWord3,
control.VectorData,
control.DwordCount);
return true;
}
if (instruction.Opcode.Contains("Format", StringComparison.Ordinal))
{
error = $"device buffer descriptor operation {instruction.Opcode} is not supported";
return false;
}
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
{
return TryEmitDeviceDescriptorBufferAtomic(instruction, control, baseAddress, size, byteAddress, out error);
}
if (TryGetSubdwordStoreInfo(instruction.Opcode, out var storeByteCount, out var sourceShift))
{
EmitExecConditional(() =>
{
var inRange = IsDeviceBufferByteRangeInRange(size, byteAddress, storeByteCount);
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask));
EmitConditional(inRange, () =>
StoreDeviceBytes(address, LoadV(control.VectorData), storeByteCount, sourceShift, _module.ConstantBool(true)));
});
return true;
}
if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal))
{
EmitExecConditional(() =>
{
for (uint index = 0; index < control.DwordCount; index++)
{
var componentAddress = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreDeviceBufferWord(
baseAddress,
size,
componentAddress,
LoadV(control.VectorData + index));
}
});
return true;
}
if (TryGetSubdwordLoadInfo(
instruction.Opcode,
out var loadByteCount,
out var signExtend,
out var d16,
out var d16High))
{
var inRange = IsDeviceBufferByteRangeInRange(size, byteAddress, loadByteCount);
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask));
Store(_deviceBufferWordScratch, UInt(0));
EmitConditional(inRange, () => Store(
_deviceBufferWordScratch,
LoadSubdwordDeviceValue(
address,
LoadV(control.VectorData),
loadByteCount,
signExtend,
d16,
d16High)));
StoreV(control.VectorData, Load(_uintType, _deviceBufferWordScratch));
return true;
}
if (instruction.Opcode.StartsWith("BufferLoadDword", StringComparison.Ordinal))
{
for (uint index = 0; index < control.DwordCount; index++)
{
var componentAddress = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreV(
control.VectorData + index,
LoadDeviceBufferWord(baseAddress, size, componentAddress));
}
return true;
}
error = $"unsupported device buffer descriptor opcode {instruction.Opcode}";
return false;
}
private bool TryEmitDeviceDescriptorBufferAtomic(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
uint baseAddress,
uint size,
uint byteAddress,
out string error)
{
error = string.Empty;
var atomicSuffix = instruction.Opcode["BufferAtomic".Length..];
if (atomicSuffix is "SwapX2" or "OrX2")
{
var atomicOp = atomicSuffix == "SwapX2" ? SpirvOp.AtomicExchange : SpirvOp.AtomicOr;
EmitExecConditional(() =>
{
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, 2 * sizeof(uint));
var firstAddress = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
var secondAddress = And64(IAdd64(baseAddress, Widen(IAdd(byteAddress, UInt(sizeof(uint))))), ULong(DeviceAddressMask & ~3ul));
EmitConditional(valid, () =>
{
var (firstPointer, firstMapped) = ResolveDeviceAddress(firstAddress);
var (secondPointer, secondMapped) = ResolveDeviceAddress(secondAddress);
var bothMapped = LogicalAnd(firstMapped, secondMapped);
EmitConditional(bothMapped, () =>
{
var originalLow = EmitAtomic(
atomicOp, _uintType, DeviceWordPointer(firstPointer), 1, 0x48,
() => LoadV(control.VectorData), () => UInt(0));
var originalHigh = EmitAtomic(
atomicOp, _uintType, DeviceWordPointer(secondPointer), 1, 0x48,
() => LoadV(control.VectorData + 1), () => UInt(0));
if (control.Glc)
{
StoreV(control.VectorData, originalLow);
StoreV(control.VectorData + 1, originalHigh);
}
});
});
});
return true;
}
if (atomicSuffix is "Fmin" or "Fmax")
{
EmitExecConditional(() =>
{
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, sizeof(uint));
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
EmitConditional(valid, () =>
{
var (pointer, mapped) = ResolveDeviceAddress(address);
EmitConditional(mapped, () =>
{
var original = EmitBufferFloatAtomic(
DeviceWordPointer(pointer),
LoadV(control.VectorData),
maxValue: atomicSuffix == "Fmax",
scope: 1,
semantics: 0x48);
if (control.Glc)
StoreV(control.VectorData, original);
});
});
});
return true;
}
if (!TryGetAtomicOp(atomicSuffix, out var atomicOperation))
{
error = $"unsupported buffer atomic opcode {instruction.Opcode}";
return false;
}
EmitExecConditional(() =>
{
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, sizeof(uint));
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
EmitConditional(valid, () =>
{
var (pointer, mapped) = ResolveDeviceAddress(address);
EmitConditional(mapped, () =>
{
var original = EmitAtomic(
atomicOperation,
_uintType,
DeviceWordPointer(pointer),
1,
0x48,
() => LoadV(control.VectorData),
() => LoadV(control.VectorData + 1));
if (control.Glc)
StoreV(control.VectorData, original);
});
});
});
return true;
}
private void EmitDeviceBufferFormatLoad(
uint baseAddress,
uint size,
uint byteAddress,
uint descriptorWord3,
uint vectorData,
uint componentCount)
{
var unifiedFormat = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(12)),
UInt(0x7F));
var (dataFormat, numberFormat) = DecodeGfx10BufferFormat(unifiedFormat);
var canonical = new uint[4];
var componentBounds = new uint[4];
for (var component = 0; component < canonical.Length; component++)
{
canonical[component] = LoadGfx10DeviceBufferFormatComponent(
baseAddress,
size,
byteAddress,
dataFormat,
numberFormat,
component,
out componentBounds[component]);
}
var selectors = new uint[componentCount];
var inBounds = _module.ConstantBool(true);
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(destination * 3)),
UInt(7));
selectors[destination] = selector;
var selectedInBounds = _module.ConstantBool(true);
for (uint component = 0; component < 4; component++)
{
selectedInBounds = _module.AddInstruction(
SpirvOp.Select,
_boolType,
_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(component + 4)),
componentBounds[component],
selectedInBounds);
}
inBounds = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, inBounds, selectedInBounds);
}
var one = Gfx10FormatOne(numberFormat);
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = selectors[destination];
var constant = SelectUInt(selector, 1, one, UInt(0));
var value = constant;
value = SelectUInt(selector, 4, canonical[0], value);
value = SelectUInt(selector, 5, canonical[1], value);
value = SelectUInt(selector, 6, canonical[2], value);
value = SelectUInt(selector, 7, canonical[3], value);
StoreV(
vectorData + destination,
_module.AddInstruction(SpirvOp.Select, _uintType, inBounds, value, constant));
}
}
private uint LoadGfx10DeviceBufferFormatComponent(
uint baseAddress,
uint size,
uint elementAddress,
uint dataFormat,
uint numberFormat,
int component,
out uint componentInBounds)
{
var byteOffset = UInt(0);
var bitOffset = UInt(0);
var bitCount = UInt(0);
void SetLayout(uint format, uint bytes, uint bits, uint count)
{
var matches = _module.AddInstruction(SpirvOp.IEqual, _boolType, dataFormat, UInt(format));
byteOffset = _module.AddInstruction(SpirvOp.Select, _uintType, matches, UInt(bytes), byteOffset);
bitOffset = _module.AddInstruction(SpirvOp.Select, _uintType, matches, UInt(bits), bitOffset);
bitCount = _module.AddInstruction(SpirvOp.Select, _uintType, matches, UInt(count), bitCount);
}
foreach (var layout in Gfx10UnifiedFormat.ComponentLayouts)
{
if (layout.Component == (uint)component)
SetLayout(layout.DataFormat, layout.ByteOffset, layout.BitOffset, layout.BitCount);
}
var componentAddress = IAdd(elementAddress, byteOffset);
var componentBytes = ShiftRightLogical(IAdd(IAdd(bitOffset, bitCount), UInt(7)), UInt(3));
var lastByteOffset = _module.AddInstruction(SpirvOp.ISub, _uintType, componentBytes, UInt(1));
var hasComponent = _module.AddInstruction(SpirvOp.INotEqual, _boolType, bitCount, UInt(0));
var inRange = IsDeviceBufferElementInRange(size, componentAddress, lastByteOffset);
var accessAllowed = LogicalAnd(hasComponent, inRange);
componentInBounds = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
LogicalNot(hasComponent),
inRange);
var packed = UInt(0);
for (uint index = 0; index < sizeof(uint); index++)
{
var address = index == 0 ? componentAddress : IAdd(componentAddress, UInt(index));
var word = LoadDeviceBufferWord(baseAddress, size, address, accessAllowed);
var shift = ShiftLeftLogical(BitwiseAnd(address, UInt(3)), UInt(3));
var value = BitwiseAnd(ShiftRightLogical(word, shift), UInt(0xFF));
packed = BitwiseOr(packed, ShiftLeftLogical(value, UInt(index * 8)));
}
var raw = _module.AddInstruction(SpirvOp.BitFieldUExtract, _uintType, packed, bitOffset, bitCount);
var converted = ConvertGfx10BufferComponent(raw, bitCount, numberFormat, dataFormat);
var valid = _module.AddInstruction(SpirvOp.INotEqual, _boolType, bitCount, UInt(0));
return _module.AddInstruction(
SpirvOp.Select,
_uintType,
valid,
converted,
component == 3 ? Gfx10FormatOne(numberFormat) : UInt(0));
}
private void EmitBufferFormatLoad(
int bindingIndex,
uint byteAddress,

View File

@ -832,6 +832,7 @@ public static partial class Gen5ShaderTranslator
0x43 => "VMovrelsB32",
0x44 => "VMovrelsdB32",
0x48 => "VMovrelsd2B32",
0x52 => "VCvtU16F16",
0x56 => "VRsqF16",
_ => string.Empty,
};
@ -1170,6 +1171,8 @@ public static partial class Gen5ShaderTranslator
0x36D => "VAdd3U32",
0x36F => "VLshlOrU32",
0x178 => "VXor3B32",
// VOP1 opcode 0x52 is available through VOP3 as opcode 0x1D2.
0x1D2 => "VCvtU16F16",
0x300 => "VLshrrevB64",
0x371 => "VAndOrB32",
0x372 => "VOr3U32",

View File

@ -265,6 +265,39 @@ public sealed partial class ResourceTracker
handle is { Kind: ScalarValueKind.BufferHandle, Operands.Length: 4 } &&
handle.Operands.All(dword => dword.Type == ScalarValueType.U32 && _plan.ValidateRuntimeValue(dword));
private bool IsDeviceLoadedBufferHandle(ScalarValue? handle) =>
handle is { Kind: ScalarValueKind.BufferHandle, Operands.Length: 4 } &&
handle.Operands.All(dword =>
dword.Type == ScalarValueType.U32 &&
(_plan.ValidateRuntimeValue(dword) || DependsOnScalarBufferWord(dword))) &&
handle.Operands.Any(DependsOnScalarBufferWord);
private static bool DependsOnScalarBufferWord(ScalarValue value)
{
var pending = new Stack<ScalarValue>();
var visited = new HashSet<ScalarValue>();
pending.Push(value);
while (pending.TryPop(out var current))
{
if (!visited.Add(current))
{
continue;
}
if (current.Kind == ScalarValueKind.ScalarBufferWord)
{
return true;
}
foreach (var operand in current.Operands)
{
pending.Push(operand);
}
}
return false;
}
private uint GetHandleSource(ScalarValue? handle, ScalarValueKind expected, uint width, uint pc, bool sampler = false, bool sampleAdjust = false)
{
if (handle is null || handle.Kind != expected)
@ -474,9 +507,10 @@ public sealed partial class ResourceTracker
if (isBuffer)
{
// A scalar buffer descriptor built from data the shader loads itself cannot be
// bound by the host; the device reads through the descriptor in registers.
if (memory.Kind == MemoryResourceKind.ScalarBuffer && !IsHostBufferHandle(access.Handle))
// Scalar loads can address buffers that have no host descriptor binding, while
// vector buffer descriptors loaded from scalar-buffer data must stay device-side.
if ((memory.Kind == MemoryResourceKind.ScalarBuffer && !IsHostBufferHandle(access.Handle)) ||
(memory.Kind == MemoryResourceKind.Buffer && IsDeviceLoadedBufferHandle(access.Handle)))
{
memory.DeviceDescriptor = true;
_info.UsesDeviceAddresses = true;

View File

@ -0,0 +1,182 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Buffers.Binary;
using Xunit;
namespace SharpEmu.Libs.Tests.Ampr;
[Collection("AmprFileRegistry")]
public sealed class AmprWaitOnAddressTests
{
[Fact]
public void MeasureCommandSizeWaitOnAddress_UsesReferenceWidth()
{
const string nid = "jIlc4p5dSD0";
const ulong memoryBase = 0x2_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
var manager = CreateManagerWithExport(nid, "sceAmprMeasureCommandSizeWaitOnAddress");
Assert.Equal(8UL, Measure(manager, context, nid, memoryBase + 0x100, 0, compare: 0, flush: 0));
Assert.Equal(12UL, Measure(manager, context, nid, memoryBase + 0x100, 0xCAFE, compare: 0, flush: 0));
Assert.Equal(16UL, Measure(manager, context, nid, memoryBase + 0x100, 0x1_0000_0000, compare: 0, flush: 0));
}
[Fact]
public void MeasureCommandSizeWaitOnAddress_ValidatesModernAndLegacyEnums()
{
const ulong memoryBase = 0x2_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
var modern = CreateManagerWithExport("jIlc4p5dSD0", "sceAmprMeasureCommandSizeWaitOnAddress");
var legacy = CreateManagerWithExport("0BMj1hgG+kE", "sceAmprMeasureCommandSizeWaitOnAddress_04_00");
context[CpuRegister.Rdi] = memoryBase + 0x100;
context[CpuRegister.Rsi] = 0xCAFE;
context[CpuRegister.Rdx] = 4;
context[CpuRegister.Rcx] = 0;
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
modern.Dispatch("jIlc4p5dSD0", context));
context[CpuRegister.Rdx] = 6;
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, legacy.Dispatch("0BMj1hgG+kE", context));
Assert.Equal(12UL, context[CpuRegister.Rax]);
context[CpuRegister.Rdi] = 0;
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
legacy.Dispatch("0BMj1hgG+kE", context));
}
[Fact]
public void CommandBufferWaitOnAddress_EncodesWaitAndCompletesWhenPredicateIsSatisfied()
{
const string nid = "V7GQTEeUfhw";
const ulong memoryBase = 0x2_0000_0000;
const ulong commandBufferAddress = memoryBase + 0x100;
const ulong recordBufferAddress = memoryBase + 0x200;
const ulong waitAddress = memoryBase + 0x800;
const ulong reference = 0xCAFE;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
var manager = CreateManagerWithExport(nid, "sceAmprCommandBufferWaitOnAddress");
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = recordBufferAddress;
context[CpuRegister.Rdx] = 0x100;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
Assert.True(context.TryWriteUInt64(waitAddress, reference));
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = waitAddress;
context[CpuRegister.Rdx] = reference;
context[CpuRegister.Rcx] = 0;
context[CpuRegister.R8] = 0;
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
Assert.True(context.TryReadUInt32(commandBufferAddress + 4, out var offset));
Assert.True(context.TryReadUInt32(commandBufferAddress + 8, out var commandCount));
Assert.Equal(12U, offset);
Assert.Equal(1U, commandCount);
Span<byte> record = stackalloc byte[12];
Assert.True(memory.TryRead(recordBufferAddress, record));
var expectedHeader = (uint)((waitAddress >> 16) & 0xFFFF0000UL) | 0x0201U;
Assert.Equal(expectedHeader, BinaryPrimitives.ReadUInt32LittleEndian(record));
Assert.Equal(0x00000800U, BinaryPrimitives.ReadUInt32LittleEndian(record[4..]));
Assert.Equal(0x0000CAFEU, BinaryPrimitives.ReadUInt32LittleEndian(record[8..]));
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
AmprExports.CompleteCommandBuffer(context, commandBufferAddress));
}
[Fact]
public void CommandBufferWaitOnAddress_SupportsAllModernAndLegacyComparisons()
{
const ulong memoryBase = 0x2_0000_0000;
const ulong commandBufferAddress = memoryBase + 0x100;
const ulong recordBufferAddress = memoryBase + 0x200;
var memory = new FakeCpuMemory(memoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
var modern = CreateManagerWithExport("V7GQTEeUfhw", "sceAmprCommandBufferWaitOnAddress");
var legacy = CreateManagerWithExport("DLfoNxTFNVk", "sceAmprCommandBufferWaitOnAddress_04_00");
var comparisons = new (uint Mode, ulong Value, ulong Reference, bool Legacy)[]
{
(0, 10, 10, false),
(1, 11, 10, false),
(2, 9, 10, false),
(3, 11, 10, false),
(4, 0, ulong.MaxValue, true),
(5, 1, 0, true),
(6, ulong.MaxValue, 0, true),
};
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = recordBufferAddress;
context[CpuRegister.Rdx] = 0x200;
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
for (var index = 0; index < comparisons.Length; index++)
{
var comparison = comparisons[index];
var address = memoryBase + 0x800 + (ulong)index * sizeof(ulong);
Assert.True(context.TryWriteUInt64(address, comparison.Value));
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = address;
context[CpuRegister.Rdx] = comparison.Reference;
context[CpuRegister.Rcx] = comparison.Mode;
context[CpuRegister.R8] = 0;
var manager = comparison.Legacy ? legacy : modern;
var nid = comparison.Legacy ? "DLfoNxTFNVk" : "V7GQTEeUfhw";
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
}
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
AmprExports.CompleteCommandBuffer(context, commandBufferAddress));
}
[Fact]
public void CommandBufferWaitOnAddress0400_RegistersLegacyVariant()
{
const string nid = "DLfoNxTFNVk";
var manager = CreateManagerWithExport(nid, "sceAmprCommandBufferWaitOnAddress_04_00");
Assert.True(manager.TryGetExport(nid, out var export));
Assert.Equal("libSceAmpr", export.LibraryName);
Assert.Equal(Generation.Gen5, export.Target);
}
private static ulong Measure(
ModuleManager manager,
CpuContext context,
string nid,
ulong address,
ulong reference,
ulong compare,
ulong flush)
{
context[CpuRegister.Rdi] = address;
context[CpuRegister.Rsi] = reference;
context[CpuRegister.Rdx] = compare;
context[CpuRegister.Rcx] = flush;
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
return context[CpuRegister.Rax];
}
private static ModuleManager CreateManagerWithExport(string nid, string exportName)
{
var manager = new ModuleManager();
manager.RegisterExports(
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
Assert.Equal(exportName, export.Name);
Assert.Equal("libSceAmpr", export.LibraryName);
Assert.Equal(Generation.Gen5, export.Target);
return manager;
}
}

View File

@ -51,6 +51,31 @@ public sealed class AjmExportsTests : IDisposable
Assert.Equal(InvalidInstance, DestroyInstance(contextId, 0x4001));
}
[Fact]
public void Initialize_Gen5AcceptsOpaqueInitializationValueAndWritesContext()
{
_ctx[CpuRegister.Rdi] = 0x0000000300000000;
_ctx[CpuRegister.Rsi] = ContextAddress;
Assert.Equal(0, AjmExports.AjmInitialize(_ctx));
var contextId = ReadUInt32(ContextAddress);
Assert.NotEqual(0u, contextId);
Assert.Equal(0, RegisterCodec(contextId, 1));
}
[Fact]
public void Initialize_Gen4StillRejectsNonzeroReservedValue()
{
var context = new CpuContext(_memory, Generation.Gen4);
const uint sentinel = 0xCCCCCCCC;
WriteUInt32(ContextAddress, sentinel);
context[CpuRegister.Rdi] = 0x0000000300000000;
context[CpuRegister.Rsi] = ContextAddress;
Assert.Equal(unchecked((int)0x806A0001), AjmExports.AjmInitialize(context));
Assert.Equal(sentinel, ReadUInt32(ContextAddress));
}
[Fact]
public void InstanceCreate_UnregisteredCodecDoesNotWriteOutput()
{

View File

@ -211,9 +211,19 @@ public sealed class RegisterWriteTableTests
Assert.Equal(0xFu, banks.Context.RenderTargetMask);
Assert.Equal(0x1_0000ul, banks.Shader.Pixel.Address);
Assert.Equal(1u, banks.IndexTypeAndSize);
var error = Assert.Throws<InvalidOperationException>(() => RegisterWriteTable.WriteContextEntry(banks, 0x1B2, 1, 0x5000));
Assert.Contains("table register is not supported", error.Message);
Assert.Contains("table=0x0000000000005000", error.Message);
RegisterWriteTable.WriteContextEntry(banks, 0x024A, 0, 0x5000);
Assert.True(banks.Context.UnmodeledTableRegisters.TryGetValue(0x024A, out var value));
Assert.Equal(0u, value);
var clone = banks.Clone();
RegisterWriteTable.WriteContextEntry(banks, 0x024A, 1, 0x5000);
Assert.Equal(0u, clone.Context.UnmodeledTableRegisters[0x024A]);
Assert.Equal(1u, banks.Context.UnmodeledTableRegisters[0x024A]);
banks.ApplyContextState(ContextStateOperation.Push);
RegisterWriteTable.WriteContextEntry(banks, 0x024A, 2, 0x5000);
banks.ApplyContextState(ContextStateOperation.Pop);
Assert.Equal(1u, banks.Context.UnmodeledTableRegisters[0x024A]);
}
// Registers without a decoded field are stored so a title that writes them does not stop.

View File

@ -104,6 +104,33 @@ public sealed class SpirvBindingDeclarationTests
Assert.DoesNotContain(request.Bindings.Descriptors, descriptor => descriptor.Kind == DescriptorBindingKind.DeviceAddressPageTable);
}
[Fact]
public void BufferDescriptorLoadedByShader_UsesDeviceAddressAccess()
{
var program = Program(
ReadFirstLane(0, 12, 0),
MoveScalar(4, 13, 0),
MoveScalar(8, 14, 16),
MoveScalar(12, 15, 0),
ScalarBufferLoad(16, 12, destination: 20, count: 4),
BufferLoad(24, 20, dwords: 4),
BufferLoad(32, 20, dwords: 3, formatted: true),
EndProgram(40));
var request = Request(program);
var memories = request.Memory.Entries.Where(entry => entry.Pc is 24 or 32).ToArray();
Assert.Equal(2, memories.Length);
Assert.All(memories, memory => Assert.True(
memory.DeviceDescriptor,
$"kind={memory.Kind} resource={memory.Resource} planningOnly={memory.PlanningOnly}"));
Assert.True(request.Resources.Info.UsesDeviceAddresses);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
var module = new SpirvModuleInspector(shader.Spirv);
Assert.Contains((uint)SpirvCapability.PhysicalStorageBufferAddresses, module.Capabilities);
Assert.Contains((ushort)SpirvOp.ConvertUToPtr, module.Opcodes);
}
[Fact]
public void SpilledUserData_ReadsTheShaderDataBufferInsteadOfPushData()
{