diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs index 5671e88..e940a1d 100644 --- a/src/SharpEmu.Libs/Ampr/AmprExports.cs +++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs @@ -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 ReadFileCommands = []; public readonly List KernelEventCommands = []; public readonly List WriteAddressCommands = []; + public readonly List 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 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 record) { if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) diff --git a/src/SharpEmu.Libs/Audio/AjmExports.cs b/src/SharpEmu.Libs/Audio/AjmExports.cs index fe5c077..08cab11 100644 --- a/src/SharpEmu.Libs/Audio/AjmExports.cs +++ b/src/SharpEmu.Libs/Audio/AjmExports.cs @@ -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; diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs index ba2acf7..5ad7830 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs @@ -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 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(UnmodeledTableRegisters); return copy; } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs index ac716e0..3592016 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs @@ -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"); diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs index eaf7b8a..936be1d 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs @@ -239,6 +239,8 @@ public static partial class Gen5MslTranslator "VCvtF32I32" => FloatResult(instruction, $"(float)as_type({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)})"), diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 4ca62c9..d9e5f95 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -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": diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs index cf87259..2ccf92c 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs @@ -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; diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index ddedb51..ab38204 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -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, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 150756f..d54f618 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -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", diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs index 05040a0..698e1f6 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs @@ -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(); + var visited = new HashSet(); + 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; diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AmprWaitOnAddressTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AmprWaitOnAddressTests.cs new file mode 100644 index 0000000..aa369d6 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Ampr/AmprWaitOnAddressTests.cs @@ -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 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; + } +} diff --git a/tests/SharpEmu.Libs.Tests/Audio/AjmExportsTests.cs b/tests/SharpEmu.Libs.Tests/Audio/AjmExportsTests.cs index 3ce1fb7..49532b3 100644 --- a/tests/SharpEmu.Libs.Tests/Audio/AjmExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Audio/AjmExportsTests.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs index ca1ee1b..c93f259 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/Registers/RegisterWriteTableTests.cs @@ -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(() => 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. diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs index e9dc8cc..d493b24 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs @@ -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() {