diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 2f86c24..fa6dfd9 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -210,6 +210,13 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80); cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88); cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; + cpuContext.SetImportStackArguments( + ReadImportStackArgument(argPackPtr, 0), + ReadImportStackArgument(argPackPtr, 1), + ReadImportStackArgument(argPackPtr, 2), + ReadImportStackArgument(argPackPtr, 3), + ReadImportStackArgument(argPackPtr, 4), + ReadImportStackArgument(argPackPtr, 5)); ulong value = cpuContext[CpuRegister.Rdi]; ulong value2 = cpuContext[CpuRegister.Rsi]; ulong num3 = cpuContext[CpuRegister.Rdx]; @@ -1324,6 +1331,13 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80); cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88); cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; + cpuContext.SetImportStackArguments( + ReadImportStackArgument(argPackPtr, 0), + ReadImportStackArgument(argPackPtr, 1), + ReadImportStackArgument(argPackPtr, 2), + ReadImportStackArgument(argPackPtr, 3), + ReadImportStackArgument(argPackPtr, 4), + ReadImportStackArgument(argPackPtr, 5)); if (_activeGuestThreadState is { } activeGuestThreadState) { @@ -1584,7 +1598,7 @@ public sealed partial class DirectExecutionBackend "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor "zgXifHT9ErY" or // sceVideoOutIsFlipPending "V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress - "qj7QZpgr9Uw" or // Gen5 graphics type-2 packet + "qj7QZpgr9Uw" or // Graphics context-state operation "LtTouSCZjHM" or // sceAgcCbNop "k3GhuSNmBLU" or // sceAgcCbDispatch "UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 413bcf0..1de1af5 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -1823,7 +1823,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private static bool IsHlePreferredNid(string nid) { return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) || - string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal); + string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal) || + string.Equals(nid, "AV6ipCNa4Rw", StringComparison.Ordinal); } private static bool IsLibcLibrary(string libraryName) diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 6e42c54..e496f98 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -140,12 +140,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA private GuestSpaceOwner? _backedSpace; public PhysicalVirtualMemory(IHostMemory? hostMemory = null, IHostViewMemory? viewHost = null, - ulong backingBytes = GuestMemoryLayout.BackingBytes) + ulong backingBytes = GuestMemoryLayout.BackingBytes, bool preReserveGuestAddressSpace = false) { _hostMemory = hostMemory ?? CrossPlatformHostMemory.Instance; if (viewHost != null) { - _backedSpace = new GuestSpaceOwner(viewHost, backingBytes); + _backedSpace = new GuestSpaceOwner(viewHost, backingBytes, preReserveGuestAddressSpace); RunBackingSelfTest(); } } diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index 8f0be7f..41022d2 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -90,13 +90,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime ImportTraceLimit = Math.Max(0, options.ImportTraceLimit), DebugHook = options.DebugHook, }; + var virtualMemory = new PhysicalVirtualMemory( + viewHost: HostViewMemory.Create(), + preReserveGuestAddressSpace: true); var moduleManager = new ModuleManager(); // The compile-time generated registry (SharpEmu.SourceGenerators) is the sole // registration source; content tests in SharpEmu.Libs.Tests pin its invariants. moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5)); moduleManager.Freeze(); - var virtualMemory = new PhysicalVirtualMemory(viewHost: HostViewMemory.Create()); var gpuMemory = new GuestGpuMemory(virtualMemory); GuestGpuMemoryHook.Attach(gpuMemory); diff --git a/src/SharpEmu.HLE/CpuContext.cs b/src/SharpEmu.HLE/CpuContext.cs index c4eb281..23c39b1 100644 --- a/src/SharpEmu.HLE/CpuContext.cs +++ b/src/SharpEmu.HLE/CpuContext.cs @@ -12,6 +12,8 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation) private readonly ulong[] _registers = new ulong[16]; private readonly ulong[] _xmmRegisters = new ulong[32]; private readonly ulong[] _ymmUpperRegisters = new ulong[32]; + private readonly ulong[] _importStackArguments = new ulong[6]; + private int _importStackArgumentCount; private bool _raxWritten; public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory)); @@ -61,6 +63,40 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation) public bool WasRaxWritten => _raxWritten; + public void SetImportStackArguments( + ulong arg0, + ulong arg1, + ulong arg2, + ulong arg3, + ulong arg4, + ulong arg5) + { + _importStackArguments[0] = arg0; + _importStackArguments[1] = arg1; + _importStackArguments[2] = arg2; + _importStackArguments[3] = arg3; + _importStackArguments[4] = arg4; + _importStackArguments[5] = arg5; + _importStackArgumentCount = _importStackArguments.Length; + } + + public void ClearImportStackArguments() + { + _importStackArgumentCount = 0; + } + + public bool TryGetImportStackArgument(int index, out ulong value) + { + if ((uint)index < (uint)_importStackArgumentCount) + { + value = _importStackArguments[index]; + return true; + } + + value = 0; + return false; + } + public void GetXmmRegister(int registerIndex, out ulong low, out ulong high) { if ((uint)registerIndex >= 16) diff --git a/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs b/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs index a6d26ad..883b5b8 100644 --- a/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs +++ b/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs @@ -19,6 +19,9 @@ public sealed class GuestSpaceOwner : IDisposable internal static Action OnFatal = message => Environment.FailFast(message); public const ulong GuestPage = 0x4000; + private const ulong UserAddressStart = 0x10_0000_0000; + private const ulong UserAddressEnd = 0xFC_0000_0000; + private const ulong MinimumPreReservedRange = 0x0100_0000; private readonly IHostViewMemory _host; private readonly SharedBackingViews _views; @@ -29,13 +32,21 @@ public sealed class GuestSpaceOwner : IDisposable private readonly List<(ulong Address, ulong Size)> _owned = new(); private bool _disposed; - public GuestSpaceOwner(IHostViewMemory host, ulong backingSize) + public GuestSpaceOwner(IHostViewMemory host, ulong backingSize, bool preReserveGuestAddressSpace = false) { _host = host; Granularity = host.Granularity; // Create lookup views before concurrent fault handlers can read the range table. _ = _mapped.Keys; _ = _mapped.Values; + if (preReserveGuestAddressSpace) + { + foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange)) + { + _owned.Add((range.Address, range.Size)); + AddFreeRange(range.Address, range.Size); + } + } _views = new SharedBackingViews(host, backingSize); if (!_views.IsAvailable) { diff --git a/src/SharpEmu.HLE/Host/IHostViewMemory.cs b/src/SharpEmu.HLE/Host/IHostViewMemory.cs index b330d0c..7f05f96 100644 --- a/src/SharpEmu.HLE/Host/IHostViewMemory.cs +++ b/src/SharpEmu.HLE/Host/IHostViewMemory.cs @@ -3,6 +3,8 @@ namespace SharpEmu.HLE.Host; +public readonly record struct HostAddressRange(ulong Address, ulong Size); + // The caller must own each range. The host does not check this on all platforms. public interface IHostViewMemory { @@ -14,6 +16,8 @@ public interface IHostViewMemory ulong ReserveHole(ulong address, ulong size); + IReadOnlyList ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize) => []; + bool SplitHole(ulong address, ulong size); bool JoinHoles(ulong address, ulong size); diff --git a/src/SharpEmu.HLE/Host/Windows/WindowsHostViews.cs b/src/SharpEmu.HLE/Host/Windows/WindowsHostViews.cs index 9017241..9633b05 100644 --- a/src/SharpEmu.HLE/Host/Windows/WindowsHostViews.cs +++ b/src/SharpEmu.HLE/Host/Windows/WindowsHostViews.cs @@ -13,6 +13,7 @@ internal sealed unsafe partial class WindowsHostViews : IHostViewMemory private const uint MEM_COMMIT = 0x1000; private const uint MEM_RESERVE = 0x2000; private const uint MEM_RELEASE = 0x8000; + private const uint MEM_FREE = 0x10000; private const uint MEM_COALESCE_PLACEHOLDERS = 0x1; private const uint MEM_PRESERVE_PLACEHOLDER = 0x2; private const uint MEM_REPLACE_PLACEHOLDER = 0x4000; @@ -88,6 +89,47 @@ internal sealed unsafe partial class WindowsHostViews : IHostViewMemory return address; } + public IReadOnlyList ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize) + { + var reserved = new List(); + if (start >= end || minimumSize == 0) + { + return reserved; + } + + var current = start; + while (current < end) + { + if (VirtualQuery((void*)current, out var info, (nuint)sizeof(MemoryBasicInformation)) == 0) + { + break; + } + + var regionEnd = Math.Min(end, info.BaseAddress + info.RegionSize); + if (info.State == MEM_FREE) + { + var reserveStart = AlignUp(Math.Max(start, info.BaseAddress), Granularity); + var reserveEnd = AlignDown(regionEnd, Granularity); + if (reserveEnd > reserveStart && reserveEnd - reserveStart >= minimumSize) + { + var size = reserveEnd - reserveStart; + if (ReserveHole(reserveStart, size) == reserveStart) + { + reserved.Add(new HostAddressRange(reserveStart, size)); + } + } + } + + if (regionEnd <= current) + { + break; + } + current = regionEnd; + } + + return reserved; + } + public bool SplitHole(ulong address, ulong size) => VirtualFree((void*)address, (nuint)size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); @@ -212,6 +254,14 @@ internal sealed unsafe partial class WindowsHostViews : IHostViewMemory return true; } + private static ulong AlignUp(ulong value, ulong alignment) + { + var remainder = value % alignment; + return remainder == 0 ? value : value + (alignment - remainder); + } + + private static ulong AlignDown(ulong value, ulong alignment) => value - value % alignment; + private static void ReleaseBackingObject(HostBackingObject backing) { UnmapViewOfFile((void*)backing.AliasBase); diff --git a/src/SharpEmu.Libs/Agc/AgcExports.CommandStream.cs b/src/SharpEmu.Libs/Agc/AgcExports.CommandStream.cs index af48897..73abc74 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.CommandStream.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.CommandStream.cs @@ -233,6 +233,10 @@ public static partial class AgcExports internal void RetainTargetlessDraw(RegisterBanks banks, in TargetlessDrawArguments arguments) { var state = RequireCurrent(); + if (RenderTrace.Enabled) + { + RenderTrace.Write($"TargetlessRetention submit={arguments.SubmitId} replaced={state.RetainedTargetlessDraw is not null} previousSubmit={state.RetainedTargetlessDraw?.Arguments.SubmitId} export=0x{banks.Shader.Vertex.ExportAddress:X16} pixel=0x{banks.Shader.Pixel.Address:X16}"); + } state.RetainedTargetlessDraw = new RetainedTargetlessDraw(banks.Clone(), arguments); } @@ -352,7 +356,7 @@ public static partial class AgcExports } - public void Dispatch(ulong submitId, uint endX, uint endY, uint endZ, uint dispatchInitiator) + public void Dispatch(ulong submitId, uint endX, uint endY, uint endZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { var state = RequireCurrent(); if (_executor is { } executor) @@ -362,7 +366,7 @@ public static partial class AgcExports var executorStarted = DcbParseProfile.Begin(); try { - executor.Dispatch(submitId, banks, endX, endY, endZ, dispatchInitiator); + executor.Dispatch(submitId, banks, endX, endY, endZ, dispatchInitiator, indirectArgumentsAddress); } finally { @@ -387,6 +391,10 @@ public static partial class AgcExports if (!VideoOutExports.TryGetDisplayBufferInfo(handle, displayBufferIndex, out var displayBuffer) || !state.KnownColorTargets.TryGetValue(displayBuffer.Address, out var words)) { + if (RenderTrace.Enabled) + { + RenderTrace.Write($"TargetlessReplay submit={retained.Arguments.SubmitId} outcome=discarded reason=display-buffer-or-target-unavailable handle={handle} index={displayBufferIndex}"); + } return; } @@ -394,6 +402,10 @@ public static partial class AgcExports banks.Context.ColorTargets[0] = words; banks.Context.RenderTargetMask = 0xF; var arguments = retained.Arguments; + if (RenderTrace.Enabled) + { + RenderTrace.Write($"TargetlessReplay submit={arguments.SubmitId} outcome=begin destination=0x{displayBuffer.Address:X16}"); + } if (arguments.Indexed) { executor.DrawIndexed(arguments.SubmitId, banks, arguments.Indexed_); @@ -403,6 +415,10 @@ public static partial class AgcExports executor.DrawAuto(arguments.SubmitId, banks, arguments.Auto); } + if (RenderTrace.Enabled) + { + RenderTrace.Write($"TargetlessReplay submit={arguments.SubmitId} outcome=executor-returned destination=0x{displayBuffer.Address:X16}"); + } TraceAgcShader( $"agc.deferred_composite dst=0x{displayBuffer.Address:X16} export=0x{banks.Shader.Vertex.ExportAddress:X16} " + $"pixel=0x{banks.Shader.Pixel.Address:X16} size={displayBuffer.Width}x{displayBuffer.Height}"); @@ -491,8 +507,8 @@ public static partial class AgcExports public override void DrawAuto(ulong submitId, in DrawAutoArguments arguments) => Translation.DrawAuto(submitId, in arguments); - public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) => - Translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator); + public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) => + Translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress); public override void OnQueueReset(int queueId) => Translation.QueueReset(queueId); @@ -515,7 +531,7 @@ public static partial class AgcExports public override void DrawAuto(ulong submitId, in DrawAutoArguments arguments) => Translation.RecordAutoDrawState(in arguments); - public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public override void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { Dispatches.Add((submitId, groupsX, groupsY, groupsZ, dispatchInitiator)); } diff --git a/src/SharpEmu.Libs/Agc/AgcExports.ControlFlow.cs b/src/SharpEmu.Libs/Agc/AgcExports.ControlFlow.cs index 1c3e0cb..4f57b86 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.ControlFlow.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.ControlFlow.cs @@ -157,9 +157,8 @@ public static partial class AgcExports return (int)ctx[CpuRegister.Rax]; } - // COND_EXEC gates the following execCount dwords on a 32-bit predicate in - // memory. Our submitted-packet walker skips unknown PM4 ops, so the recorded - // packet degrades to "predicate always true" — the gated commands always run. + // The interpreter reads the predicate when this packet executes. + // A zero value skips the specified number of following dwords. [SysAbiExport( Nid = "BIPexNBSGog", ExportName = "sceAgcDcbCondExec", diff --git a/src/SharpEmu.Libs/Agc/AgcExports.DmaPackets.cs b/src/SharpEmu.Libs/Agc/AgcExports.DmaPackets.cs index 98326f4..5d1c8bb 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.DmaPackets.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.DmaPackets.cs @@ -10,6 +10,58 @@ public static partial class AgcExports { // This partial constructs AGC DMA transfer packets. + [SysAbiExport( + Nid = "1rZSWUv1IRc", + ExportName = "sceAgcDcbCopyData", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbCopyData(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var destinationSelector = (uint)(ctx[CpuRegister.Rsi] & 0xFF); + var destinationPolicy = (uint)(ctx[CpuRegister.Rdx] & 0xFF); + var destinationAddress = ctx[CpuRegister.Rcx]; + var sourceSelector = (uint)(ctx[CpuRegister.R8] & 0xFF); + var sourcePolicy = (uint)(ctx[CpuRegister.R9] & 0xFF); + var stackAddress = ctx[CpuRegister.Rsp]; + + if (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var sourceValue) || + !TryReadUInt64(ctx, stackAddress + (2 * sizeof(ulong)), out var elementSizeRaw) || + !TryReadUInt64(ctx, stackAddress + (3 * sizeof(ulong)), out var confirmWriteRaw) || + commandBufferAddress == 0 || + !TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var packetAddress)) + { + return ReturnPointer(ctx, 0); + } + + var elementSize = (uint)(elementSizeRaw & 0xFF); + var confirmWrite = (uint)(confirmWriteRaw & 0xFF); + var control = + ((sourceSelector >> 1) & 0xFu) | + (((destinationSelector >> 1) & 0xFu) << 8) | + ((sourcePolicy & 0x3u) << 13) | + ((elementSize & 0x1u) << 16) | + ((confirmWrite & 0x1u) << 20) | + ((destinationPolicy & 0x3u) << 25) | + ((sourceSelector & 0x1u) << 30); + + if (!TryWriteUInt32(ctx, packetAddress, Pm4(6, ItCopyData, 0)) || + !TryWriteUInt32(ctx, packetAddress + 4, control) || + !TryWriteUInt32(ctx, packetAddress + 8, (uint)sourceValue) || + !TryWriteUInt32(ctx, packetAddress + 12, (uint)(sourceValue >> 32)) || + !TryWriteUInt32(ctx, packetAddress + 16, (uint)destinationAddress) || + !TryWriteUInt32(ctx, packetAddress + 20, (uint)(destinationAddress >> 32))) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc( + $"agc.dcb_copy_data buf=0x{commandBufferAddress:X16} cmd=0x{packetAddress:X16} " + + $"dstSel=0x{destinationSelector:X2} srcSel=0x{sourceSelector:X2} " + + $"dst=0x{destinationAddress:X16} src=0x{sourceValue:X16} control=0x{control:X8}"); + return ReturnPointer(ctx, packetAddress); + } + [SysAbiExport( Nid = "WmAc2MEj6Io", ExportName = "sceAgcDcbDmaData", diff --git a/src/SharpEmu.Libs/Agc/AgcExports.DriverResources.cs b/src/SharpEmu.Libs/Agc/AgcExports.DriverResources.cs index 32b02bb..0e100c0 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.DriverResources.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.DriverResources.cs @@ -267,7 +267,7 @@ public static partial class AgcExports public static int DriverUnregisterOwnerAndResources(CpuContext ctx) { var owner = (uint)ctx[CpuRegister.Rdi]; - var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); + var state = _submittedGpuStates.GetValue(CanonicalMemory(ctx.Memory), static _ => new SubmittedGpuState()); int resources; lock (state.Gate) { @@ -292,7 +292,7 @@ public static partial class AgcExports public static int DriverUnregisterAllResourcesForOwner(CpuContext ctx) { var owner = (uint)ctx[CpuRegister.Rdi]; - var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); + var state = _submittedGpuStates.GetValue(CanonicalMemory(ctx.Memory), static _ => new SubmittedGpuState()); int resources; lock (state.Gate) { diff --git a/src/SharpEmu.Libs/Agc/AgcExports.Submissions.cs b/src/SharpEmu.Libs/Agc/AgcExports.Submissions.cs index e46ba80..b31af3c 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.Submissions.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.Submissions.cs @@ -198,20 +198,48 @@ public static partial class AgcExports ExportName = "sceAgcUnknownQj7QZpgr9Uw", Target = Generation.Gen5, LibraryName = "libSceAgc")] - public static int UnknownQj7QZpgr9Uw(CpuContext ctx) + public static int DcbContextStateOperation(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; - if (commandBufferAddress == 0 || - !TryAllocateCommandDwords(ctx, commandBufferAddress, 1, out var commandAddress) || - !TryWriteUInt32(ctx, commandAddress, 0x8000_0000)) + var operation = (uint)ctx[CpuRegister.Rsi]; + if (commandBufferAddress == 0 || operation > (uint)ContextStateOperation.PushClear) { return ReturnPointer(ctx, 0); } - TraceAgc( - $"agc.unknown_qj7 buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + - $"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}"); - return ReturnPointer(ctx, commandAddress); + ulong firstPacket = 0; + bool Append(uint dwords) + { + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwords, out var address)) + return false; + Span packet = stackalloc byte[36]; + packet.Clear(); + var first = firstPacket == 0; + BinaryPrimitives.WriteUInt32LittleEndian(packet, Pm4(dwords, PacketOpcode.Nop, first ? PacketCustomCode.ContextState : 0)); + if (first) + BinaryPrimitives.WriteUInt32LittleEndian(packet[4..], operation); + if (!ctx.Memory.TryWrite(address, packet[..(int)(dwords * sizeof(uint))])) + return false; + if (first) + firstPacket = address; + return true; + } + + bool AppendSaveRestore() => + TryPrepareCommandDwords(ctx, commandBufferAddress, 22, false, out _) && + Append(5) && Append(8) && Append(9); + + // Keep allocation boundaries while the first packet carries the context operation. + var complete = (ContextStateOperation)operation switch + { + ContextStateOperation.Clear => Append(5), + ContextStateOperation.Push => AppendSaveRestore() && Append(3) && Append(2), + ContextStateOperation.Pop => Append(3) && AppendSaveRestore() && Append(2), + ContextStateOperation.PushClear => AppendSaveRestore() && Append(3) && Append(2) && Append(5), + _ => false, + }; + TraceAgc($"agc.context_state buf=0x{commandBufferAddress:X16} operation={operation} complete={complete}"); + return ReturnPointer(ctx, complete ? firstPacket : 0); } #pragma warning restore SHEM006 diff --git a/src/SharpEmu.Libs/Agc/AgcExports.TextureDescriptors.cs b/src/SharpEmu.Libs/Agc/AgcExports.TextureDescriptors.cs index 5a64f4d..9b6de0b 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.TextureDescriptors.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.TextureDescriptors.cs @@ -201,10 +201,10 @@ public static partial class AgcExports return false; } - // Preserve the high address byte and the full width and height fields. + // Prospero T# stores width-minus-one across 14 bits and height-minus-one in 14 bits. var address = (((ulong)(fields[1] & 0xFFu) << 32) | fields[0]) << 8; - var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0x3FFFu) << 2)) + 1; - var height = ((fields[2] >> 14) & 0xFFFFu) + 1; + var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0x0FFFu) << 2)) + 1; + var height = ((fields[2] >> 14) & 0x3FFFu) + 1; var unifiedFormat = (fields[1] >> 20) & 0x1FFu; if (unifiedFormat == 0 || !Gfx10UnifiedFormat.TryDecode( diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index fc52037..e7af284 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -38,6 +38,7 @@ public static partial class AgcExports private const uint ItDispatchIndirect = 0x16; private const uint ItSetPredication = 0x20; private const uint ItCondExec = 0x22; + private const uint ItCopyData = 0x40; private const uint ItWaitRegMem = 0x3C; private const uint ItIndirectBuffer = 0x3F; private const uint ItCondWrite = 0x45; @@ -487,6 +488,9 @@ public static partial class AgcExports } private static bool TryAllocateCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, out ulong commandAddress) + => TryPrepareCommandDwords(ctx, commandBufferAddress, sizeDwords, true, out commandAddress); + + private static bool TryPrepareCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, bool advanceCursor, out ulong commandAddress) { commandAddress = 0; if (sizeDwords == 0 || @@ -542,7 +546,7 @@ public static partial class AgcExports } var nextCursor = cursorUp + ((ulong)sizeDwords * sizeof(uint)); - if (!ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor)) + if (advanceCursor && !ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor)) { return false; } diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs index 380b6c0..5671e88 100644 --- a/src/SharpEmu.Libs/Ampr/AmprExports.cs +++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs @@ -13,19 +13,33 @@ namespace SharpEmu.Libs.Ampr; public static class AmprExports { - private const int CommandBufferHeaderSize = 0x28; - private const ulong CommandBufferSelfOffset = 0x00; - private const ulong CommandBufferDataOffset = 0x08; - private const ulong CommandBufferSizeOffset = 0x10; + private const int CommandBufferHeaderSize = 0x18; + private const ulong CommandBufferTypeOffset = 0x00; + private const ulong CommandBufferOffsetOffset = 0x04; + private const ulong CommandBufferNumOffset = 0x08; + private const ulong CommandBufferSizeOffset = 0x0C; + private const ulong CommandBufferDataOffset = 0x10; private const ulong CommandBufferAux0Offset = 0x18; private const ulong CommandBufferAux1Offset = 0x20; - private const ulong ReadFileRecordSize = 0x30; - private const ulong KernelEventQueueRecordSize = 0x30; + private const ulong ReadFileRecordSize = 0x14; + private const ulong ReadFileRecordSizeExtended = 0x18; + private const ulong ReadGatherRecordSize = 0x08; + private const ulong ReadGatherRecordSizeExtended = 0x0C; + private const ulong ReadScatterRecordSize = 0x0C; + private const ulong ReadGatherScatterRecordSize = 0x10; + private const ulong ReadGatherScatterRecordSizeExtended = 0x14; + private const ulong ResetGatherScatterRecordSize = 0x04; + private const uint AprTypeGatherScatterValid = 0x00010000; + private const ulong AprMaxReadLength = 0x0000000100000000; + private const ulong AprMaxFileOffset = 0x0000010000000000; + private const ulong AprMaxAppAddress = 0x0000f00000000000; + private const ulong KernelEventQueueRecordSize = 0x20; private const ulong WriteAddressRecordSize = 0x20; - private const uint ReadFileRecordType = 1; + private const uint ReadFileRecordType = 0x17; private const uint KernelEventQueueRecordType = 2; private const uint WriteAddressRecordType = 3; private static readonly ConcurrentDictionary _commandBuffers = new(); + private static readonly ConcurrentDictionary _commandBufferAliases = new(); private static readonly bool _traceAmpr = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal); private static readonly bool _traceAmprReads = @@ -38,6 +52,38 @@ public static class AmprExports public ulong Size; public ulong WriteOffset; public ulong CommandCount; + public readonly List ReadFileCommands = []; + public readonly List KernelEventCommands = []; + public readonly List WriteAddressCommands = []; + public bool GatherScatterValid; + public uint GatherScatterFileId; + public ulong GatherScatterDestination; + public ulong GatherScatterFileOffset; + } + + private sealed class ReadFileCommand + { + public ulong RecordOffset; + public ulong RecordSize; + public uint FileId; + public ulong Destination; + public ulong Size; + public ulong FileOffset; + } + + private sealed class KernelEventCommand + { + public ulong RecordOffset; + public ulong Equeue; + public int Id; + public ulong Data; + } + + private sealed class WriteAddressCommand + { + public ulong RecordOffset; + public ulong Address; + public ulong Value; } private sealed class CachedHostFile : IDisposable @@ -83,8 +129,6 @@ public static class AmprExports public static int CommandBufferConstructor(CpuContext ctx) { var commandBuffer = ctx[CpuRegister.Rdi]; - var buffer = ctx[CpuRegister.Rsi]; - var size = ctx[CpuRegister.Rdx]; if (commandBuffer == 0) { @@ -92,14 +136,17 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0: 0, aux1: 0, clear: true)) + Span header = stackalloc byte[CommandBufferHeaderSize]; + header.Clear(); + if (!ctx.Memory.TryWrite(commandBuffer, header)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - TraceAmpr(ctx, "ctor", commandBuffer, buffer, size); + RemoveCommandBufferState(commandBuffer); + TraceAmpr(ctx, "ctor", commandBuffer, 0, 0); TryPreindexApp0(); - ctx[CpuRegister.Rax] = commandBuffer; + ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -120,7 +167,12 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false)) + var reserved0 = aux0 != 0 ? aux0 : commandBuffer + CommandBufferAux0Offset; + var reserved1 = aux1 != 0 ? aux1 : commandBuffer + CommandBufferAux1Offset; + Span zero = stackalloc byte[sizeof(ulong)]; + zero.Clear(); + if (!ctx.Memory.TryWrite(reserved0, zero) || + (reserved1 != reserved0 && !ctx.Memory.TryWrite(reserved1, zero))) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -145,13 +197,6 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - Span auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2]; - auxiliaryPointers.Clear(); - if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - TraceAmpr(ctx, "apr_dtor", commandBuffer, 0, 0); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -171,12 +216,7 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - - _commandBuffers.TryRemove(commandBuffer, out _); + RemoveCommandBufferState(commandBuffer); TraceAmpr(ctx, "dtor", commandBuffer, 0, 0); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -221,21 +261,18 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - Span bufferPointers = stackalloc byte[sizeof(ulong) * 2]; - if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers)) + if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) || + !TryReadUInt32(ctx, commandBuffer + CommandBufferSizeOffset, out var size32)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers); - var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]); - if ( - !WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0)) + if (!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size32, writeOffset: 0)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - TraceAmpr(ctx, "reset", commandBuffer, buffer, size); + TraceAmpr(ctx, "reset", commandBuffer, buffer, size32); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -261,7 +298,7 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - _commandBuffers.TryRemove(commandBuffer, out _); + RemoveCommandBufferState(commandBuffer); TraceAmpr(ctx, "clear_buffer", commandBuffer, buffer, size); ctx[CpuRegister.Rax] = buffer; return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -279,63 +316,29 @@ public static class AmprExports var destination = ctx[CpuRegister.R8]; var size = ctx[CpuRegister.R9]; - if (commandBuffer == 0 || (destination == 0 && size != 0)) + if (commandBuffer == 0 || !IsValidAprReadRange(destination, size)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out var fileOffset)) + if (!ctx.TryGetImportStackArgument(0, out var fileOffset) && + !ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out fileOffset)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath)) + // APR records the read here and performs the I/O only when the command + // buffer is submitted. Keep the sequential-offset sentinel as a SharpEmu + // compatibility extension and resolve it in CompleteReadFileRecord so + // queued reads observe command order. + if (fileOffset != ulong.MaxValue && !IsValidAprFileOffset(fileOffset)) { - // Cooked Insomniac content ids are FNV("/app0/...") and may never - // pass through APR resolve. Index app0 once, then retry the lookup. - var app0Root = KernelMemoryCompatExports.ResolveGuestPath("$/"); - if (!string.IsNullOrEmpty(app0Root)) - { - AmprFileRegistry.EnsureApp0Indexed(app0Root); - } - - if (!AmprFileRegistry.TryGetHostPath(fileId, out hostPath)) - { - TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; - } + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - // Offset -1 means "continue after the previous read of this file id". - // #216 dropped this wiring; without it sequential pack/streamer reads - // fail as INVALID_ARGUMENT and RAGE load jobs never complete while the - // North Yankton UI keeps flipping. - if (fileOffset == unchecked((ulong)(long)-1)) - { - fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size); - } - else if (fileOffset > long.MaxValue) - { - fileOffset = 0; - } - - var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead); - if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK) - { - TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result); - return result; - } - - PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead); - - if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - - TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_OK); - ctx[CpuRegister.Rax] = 0; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + return AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } [SysAbiExport( @@ -345,8 +348,185 @@ public static class AmprExports LibraryName = "libSceAmpr")] public static int MeasureCommandSizeReadFile(CpuContext ctx) { - TraceAmpr(ctx, "measure_read_file", 0, ReadFileRecordSize, 0); - ctx[CpuRegister.Rax] = ReadFileRecordSize; + var fileOffset = ctx[CpuRegister.Rcx]; + var recordSize = GetReadFileRecordSize(fileOffset); + TraceAmpr(ctx, "measure_read_file", 0, recordSize, fileOffset); + ctx[CpuRegister.Rax] = recordSize; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "qesF88X4DRg", + ExportName = "sceAmprMeasureCommandSizeReadFileGather", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int MeasureCommandSizeReadFileGather(CpuContext ctx) + { + var size = ctx[CpuRegister.Rdi]; + var fileOffset = ctx[CpuRegister.Rsi]; + ctx[CpuRegister.Rax] = IsValidAprReadSize(size) && IsValidAprFileOffset(fileOffset) + ? GetReadGatherRecordSize(fileOffset) + : unchecked((uint)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "7nXGDGMXSqo", + ExportName = "sceAmprMeasureCommandSizeReadFileScatter", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int MeasureCommandSizeReadFileScatter(CpuContext ctx) + { + var destination = ctx[CpuRegister.Rdi]; + var size = ctx[CpuRegister.Rsi]; + ctx[CpuRegister.Rax] = IsValidAprReadRange(destination, size) + ? ReadScatterRecordSize + : unchecked((uint)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "DXmgc5op8Yw", + ExportName = "sceAmprMeasureCommandSizeReadFileGatherScatter", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int MeasureCommandSizeReadFileGatherScatter(CpuContext ctx) + { + var destination = ctx[CpuRegister.Rdi]; + var size = ctx[CpuRegister.Rsi]; + var fileOffset = ctx[CpuRegister.Rdx]; + ctx[CpuRegister.Rax] = IsValidAprReadRange(destination, size) && IsValidAprFileOffset(fileOffset) + ? GetReadGatherScatterRecordSize(fileOffset) + : unchecked((uint)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "mZSbNJVJpV8", + ExportName = "sceAmprAprCommandBufferReadFileGather", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferReadFileGather(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var size = ctx[CpuRegister.Rcx]; + var fileOffset = ctx[CpuRegister.R8]; + if (commandBuffer == 0 || !IsValidAprReadSize(size) || !IsValidAprFileOffset(fileOffset) || + !TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + uint fileId; + ulong destination; + lock (state) + { + if (!state.GatherScatterValid || !IsValidAprReadRange(state.GatherScatterDestination, size)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + fileId = state.GatherScatterFileId; + destination = state.GatherScatterDestination; + } + + return AppendReadFileRecord(ctx, commandBuffer, 0x18, fileId, destination, size, fileOffset, + GetReadGatherRecordSize(fileOffset)) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "Jg-AgkdJHkk", + ExportName = "sceAmprAprCommandBufferReadFileScatter", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferReadFileScatter(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var destination = ctx[CpuRegister.Rcx]; + var size = ctx[CpuRegister.R8]; + if (commandBuffer == 0 || !IsValidAprReadRange(destination, size) || + !TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + uint fileId; + ulong fileOffset; + lock (state) + { + if (!state.GatherScatterValid || !IsValidAprFileOffset(state.GatherScatterFileOffset)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + fileId = state.GatherScatterFileId; + fileOffset = state.GatherScatterFileOffset; + } + + return AppendReadFileRecord(ctx, commandBuffer, 0x19, fileId, destination, size, fileOffset, + ReadScatterRecordSize) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "BVmR1H8l+XI", + ExportName = "sceAmprAprCommandBufferReadFileGatherScatter", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferReadFileGatherScatter(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + var destination = ctx[CpuRegister.Rcx]; + var size = ctx[CpuRegister.R8]; + var fileOffset = ctx[CpuRegister.R9]; + if (commandBuffer == 0 || !IsValidAprReadRange(destination, size) || !IsValidAprFileOffset(fileOffset) || + !TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + uint fileId; + lock (state) + { + if (!state.GatherScatterValid) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + fileId = state.GatherScatterFileId; + } + + return AppendReadFileRecord(ctx, commandBuffer, 0x1A, fileId, destination, size, fileOffset, + GetReadGatherScatterRecordSize(fileOffset)) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "YPxkUDhgoNI", + ExportName = "sceAmprAprCommandBufferResetGatherScatterState", + Target = Generation.Gen5, + LibraryName = "libSceAmpr")] + public static int AprCommandBufferResetGatherScatterState(CpuContext ctx) + { + var commandBuffer = ctx[CpuRegister.Rdi]; + if (commandBuffer == 0 || !AppendNoOpRecord(ctx, commandBuffer, ResetGatherScatterRecordSize) || + !TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + lock (state) + { + state.GatherScatterValid = false; + state.GatherScatterFileId = 0; + state.GatherScatterDestination = 0; + state.GatherScatterFileOffset = 0; + } + if (ctx.TryReadUInt32(commandBuffer + CommandBufferTypeOffset, out var type)) + { + _ = ctx.TryWriteUInt32(commandBuffer + CommandBufferTypeOffset, type & ~AprTypeGatherScatterValid); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -483,8 +663,7 @@ public static class AmprExports var commandBuffer = ctx[CpuRegister.Rdi]; var equeue = ctx[CpuRegister.Rsi]; var ident = ctx[CpuRegister.Rdx]; - var completionToken = ctx[CpuRegister.Rcx]; - var userData = ctx[CpuRegister.R8]; + var data = ctx[CpuRegister.Rcx]; if (commandBuffer == 0) { @@ -496,13 +675,12 @@ public static class AmprExports commandBuffer, equeue, ident, - completionToken, - userData)) + data)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken); + TraceAmpr(ctx, "write_equeue", commandBuffer, ident, data); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -517,8 +695,7 @@ public static class AmprExports var commandBuffer = ctx[CpuRegister.Rdi]; var equeue = ctx[CpuRegister.Rsi]; var ident = ctx[CpuRegister.Rdx]; - var completionToken = ctx[CpuRegister.Rcx]; - var userData = ctx[CpuRegister.R8]; + var data = ctx[CpuRegister.Rcx]; if (commandBuffer == 0) { @@ -530,13 +707,12 @@ public static class AmprExports commandBuffer, equeue, ident, - completionToken, - userData)) + data)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken); + TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, data); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -595,6 +771,18 @@ public static class AmprExports public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer) { + return CompleteCommandBuffer(ctx, commandBuffer, out _, out _); + } + + public static int CompleteCommandBuffer( + CpuContext ctx, + ulong commandBuffer, + out int executionResult, + out uint errorOffset) + { + executionResult = (int)OrbisGen2Result.ORBIS_GEN2_OK; + errorOffset = 0; + if (commandBuffer == 0) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; @@ -606,46 +794,72 @@ public static class AmprExports } ulong writeOffset; + ReadFileCommand[] readCommands; + KernelEventCommand[] kernelEventCommands; + WriteAddressCommand[] writeAddressCommands; lock (state) { writeOffset = state.WriteOffset; + readCommands = state.ReadFileCommands.ToArray(); + kernelEventCommands = state.KernelEventCommands.ToArray(); + writeAddressCommands = state.WriteAddressCommands.ToArray(); } - var offset = 0UL; - while (offset < writeOffset) + var commands = new List<(ulong Offset, int Kind, int Index)>( + readCommands.Length + kernelEventCommands.Length + writeAddressCommands.Length); + for (var i = 0; i < readCommands.Length; i++) { - if (!TryReadUInt32(ctx, buffer + offset, out var recordType)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } + commands.Add((readCommands[i].RecordOffset, 0, i)); + } + for (var i = 0; i < kernelEventCommands.Length; i++) + { + commands.Add((kernelEventCommands[i].RecordOffset, 1, i)); + } + for (var i = 0; i < writeAddressCommands.Length; i++) + { + commands.Add((writeAddressCommands[i].RecordOffset, 2, i)); + } + commands.Sort(static (left, right) => left.Offset.CompareTo(right.Offset)); - switch (recordType) + foreach (var commandEntry in commands) + { + switch (commandEntry.Kind) { - case ReadFileRecordType: - offset += ReadFileRecordSize; - break; - - case KernelEventQueueRecordType: - if (!CompleteKernelEventQueueRecord(ctx, buffer + offset)) + case 0: { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + var readCommand = readCommands[commandEntry.Index]; + var readResult = CompleteReadFileRecord(ctx, commandBuffer, readCommand); + if (readResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) + { + executionResult = readResult; + errorOffset = checked((uint)commandEntry.Offset); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + break; } - offset += KernelEventQueueRecordSize; - break; - - case WriteAddressRecordType: - if (!CompleteWriteAddressRecord(ctx, buffer + offset)) + case 1: + var eventCommand = kernelEventCommands[commandEntry.Index]; + if (!KernelEventQueueCompatExports.TriggerAmprEvent( + eventCommand.Equeue, + unchecked((uint)eventCommand.Id), + eventCommand.Data)) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + executionResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + errorOffset = checked((uint)commandEntry.Offset); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - - offset += WriteAddressRecordSize; break; - default: - TraceAmpr(ctx, "complete_unknown", commandBuffer, recordType, offset); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + case 2: + var writeCommand = writeAddressCommands[commandEntry.Index]; + if (!ctx.TryWriteUInt64(writeCommand.Address, writeCommand.Value)) + { + executionResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + errorOffset = checked((uint)commandEntry.Offset); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + break; } } @@ -653,43 +867,47 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - private static bool InitializeCommandBuffer( - CpuContext ctx, - ulong commandBuffer, - ulong buffer, - ulong size, - ulong aux0, - ulong aux1, - bool clear) + private static int CompleteReadFileRecord(CpuContext ctx, ulong commandBuffer, ReadFileCommand command) { - Span header = stackalloc byte[CommandBufferHeaderSize]; - if (clear) + var fileId = command.FileId; + var destination = command.Destination; + var size = command.Size; + var fileOffset = command.FileOffset; + + if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath)) { - header.Clear(); - } - else - { - if (!ctx.Memory.TryRead(commandBuffer, header)) + var app0Root = KernelMemoryCompatExports.ResolveGuestPath("$/"); + if (!string.IsNullOrEmpty(app0Root)) { - return false; + AmprFileRegistry.EnsureApp0Indexed(app0Root); } - buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]); - size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]); + if (!AmprFileRegistry.TryGetHostPath(fileId, out hostPath)) + { + TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } } - BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer); - BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer); - BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size); - BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0); - BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1); - if (!ctx.Memory.TryWrite(commandBuffer, header)) + // Resolve the sequential-read sentinel at execution time to preserve command order. + if (fileOffset == unchecked((ulong)(long)-1)) { - return false; + fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size); + } + else if (fileOffset > long.MaxValue) + { + fileOffset = 0; } - UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0); - return true; + var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead); + TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result); + if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK) + { + return result; + } + + PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size) @@ -699,7 +917,10 @@ public static class AmprExports private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size, ulong writeOffset) { - if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer, size)) + if (size > uint.MaxValue || writeOffset > uint.MaxValue || + !WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer, size) || + !ctx.TryWriteUInt32(commandBuffer + CommandBufferOffsetOffset, (uint)writeOffset) || + !ctx.TryWriteUInt32(commandBuffer + CommandBufferNumOffset, 0)) { return false; } @@ -711,11 +932,13 @@ public static class AmprExports private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size) { - Span pointers = stackalloc byte[sizeof(ulong) * 3]; - BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer); - BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer); - BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size); - return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers); + if (size > uint.MaxValue) + { + return false; + } + + return ctx.TryWriteUInt32(commandBuffer + CommandBufferSizeOffset, (uint)size) && + ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer); } private static void UpdateCommandBufferState( @@ -731,7 +954,16 @@ public static class AmprExports state.Size = size; state.WriteOffset = writeOffset; state.CommandCount = 0; + state.ReadFileCommands.Clear(); + state.KernelEventCommands.Clear(); + state.WriteAddressCommands.Clear(); + state.GatherScatterValid = false; + state.GatherScatterFileId = 0; + state.GatherScatterDestination = 0; + state.GatherScatterFileOffset = 0; } + + RegisterCommandBufferAlias(commandBuffer, buffer); } private static bool TryGetCommandBufferState( @@ -741,7 +973,7 @@ public static class AmprExports out ulong size, out CommandBufferState? state) { - if (_commandBuffers.TryGetValue(commandBuffer, out state)) + if (_commandBuffers.TryGetValue(commandBuffer, out state) && HasQueuedCommands(state)) { lock (state) { @@ -752,18 +984,49 @@ public static class AmprExports return true; } - Span pointers = stackalloc byte[sizeof(ulong) * 2]; - if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers)) + if (_commandBufferAliases.TryGetValue(commandBuffer, out var owner) && + _commandBuffers.TryGetValue(owner, out state)) { - buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers); - size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]); + lock (state) + { + buffer = state.Buffer; + size = state.Size; + } + + return true; + } + + if (state is not null) + { + lock (state) + { + buffer = state.Buffer; + size = state.Size; + } + + return true; + } + + if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) && + TryReadUInt32(ctx, commandBuffer + CommandBufferSizeOffset, out var size32) && + TryReadUInt32(ctx, commandBuffer + CommandBufferOffsetOffset, out var offset32) && + TryReadUInt32(ctx, commandBuffer + CommandBufferNumOffset, out var count32)) + { + size = size32; state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState()); lock (state) { state.Buffer = buffer; state.Size = size; - state.WriteOffset = 0; - state.CommandCount = 0; + state.WriteOffset = offset32; + state.CommandCount = count32; + state.ReadFileCommands.Clear(); + state.KernelEventCommands.Clear(); + state.WriteAddressCommands.Clear(); + state.GatherScatterValid = false; + state.GatherScatterFileId = 0; + state.GatherScatterDestination = 0; + state.GatherScatterFileOffset = 0; } return true; @@ -775,6 +1038,44 @@ public static class AmprExports return false; } + private static bool HasQueuedCommands(CommandBufferState state) + { + lock (state) + { + return state.ReadFileCommands.Count != 0 || + state.KernelEventCommands.Count != 0 || + state.WriteAddressCommands.Count != 0; + } + } + + private static void RegisterCommandBufferAlias(ulong commandBuffer, ulong buffer) + { + foreach (var alias in _commandBufferAliases) + { + if (alias.Value == commandBuffer) + { + _commandBufferAliases.TryRemove(alias.Key, out _); + } + } + + if (buffer != 0 && buffer != commandBuffer) + { + _commandBufferAliases[buffer] = commandBuffer; + } + } + + private static void RemoveCommandBufferState(ulong commandBuffer) + { + _commandBuffers.TryRemove(commandBuffer, out _); + foreach (var alias in _commandBufferAliases) + { + if (alias.Key == commandBuffer || alias.Value == commandBuffer) + { + _commandBufferAliases.TryRemove(alias.Key, out _); + } + } + } + private static bool TryGetCommandBufferOffset(CpuContext ctx, ulong commandBuffer, out ulong offset) { if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) @@ -992,18 +1293,147 @@ public static class AmprExports uint fileId, ulong destination, ulong size, - ulong fileOffset, - ulong bytesRead) + ulong fileOffset) { - Span record = stackalloc byte[(int)ReadFileRecordSize]; - record.Clear(); - BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], ReadFileRecordType); - BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], fileId); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], destination); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], size); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], fileOffset); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], bytesRead); + return AppendReadFileRecord( + ctx, + commandBuffer, + opcode: 0x17, + fileId, + destination, + size, + fileOffset, + GetReadFileRecordSize(fileOffset)); + } + private static bool AppendReadFileRecord( + CpuContext ctx, + ulong commandBuffer, + byte opcode, + uint fileId, + ulong destination, + ulong size, + ulong fileOffset, + ulong recordSize) + { + if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return false; + } + + Span record = stackalloc byte[(int)recordSize]; + record.Clear(); + record[0] = opcode; + + lock (state) + { + if (state.Buffer == 0 || + state.WriteOffset > state.Size || + recordSize > state.Size - state.WriteOffset) + { + return false; + } + + var recordOffset = state.WriteOffset; + if (!ctx.Memory.TryWrite(state.Buffer + recordOffset, record)) + { + return false; + } + + state.ReadFileCommands.Add(new ReadFileCommand + { + RecordOffset = recordOffset, + RecordSize = recordSize, + FileId = fileId, + Destination = destination, + Size = size, + FileOffset = fileOffset, + }); + state.WriteOffset += recordSize; + state.CommandCount++; + if (!WriteCommandBufferProgress(ctx, commandBuffer, state)) + { + state.ReadFileCommands.RemoveAt(state.ReadFileCommands.Count - 1); + state.WriteOffset -= recordSize; + state.CommandCount--; + return false; + } + + state.GatherScatterFileId = fileId; + if (destination > ulong.MaxValue - size) + { + state.GatherScatterValid = false; + return false; + } + + state.GatherScatterDestination = destination + size; + if (fileOffset == ulong.MaxValue) + { + // -1 means continue after the previous read of this file id. + // Its concrete file offset is not known until command execution, + // so it cannot seed a gather/scatter continuation here. + state.GatherScatterValid = false; + state.GatherScatterFileOffset = 0; + } + else + { + if (fileOffset > ulong.MaxValue - size) + { + state.GatherScatterValid = false; + return false; + } + + state.GatherScatterValid = true; + state.GatherScatterFileOffset = fileOffset + size; + } + } + + if (ctx.TryReadUInt32(commandBuffer + CommandBufferTypeOffset, out var type)) + { + var updatedType = state.GatherScatterValid + ? type | AprTypeGatherScatterValid + : type & ~AprTypeGatherScatterValid; + _ = ctx.TryWriteUInt32(commandBuffer + CommandBufferTypeOffset, updatedType); + } + + return true; + } + + private static ulong GetReadFileRecordSize(ulong fileOffset) + { + return (fileOffset >> 32) != 0 ? ReadFileRecordSizeExtended : ReadFileRecordSize; + } + + private static ulong GetReadGatherRecordSize(ulong fileOffset) + { + return fileOffset > 0x3FFFF ? ReadGatherRecordSizeExtended : ReadGatherRecordSize; + } + + private static ulong GetReadGatherScatterRecordSize(ulong fileOffset) + { + return (fileOffset >> 32) != 0 ? ReadGatherScatterRecordSizeExtended : ReadGatherScatterRecordSize; + } + + private static bool IsValidAprFileOffset(ulong fileOffset) => fileOffset < AprMaxFileOffset; + + private static bool IsValidAprReadSize(ulong size) => size != 0 && size <= AprMaxReadLength; + + private static bool IsValidAprReadRange(ulong destination, ulong size) + { + return IsValidAprReadSize(size) && + destination <= AprMaxAppAddress && + AprMaxAppAddress - destination >= size; + } + + private static bool AppendNoOpRecord(CpuContext ctx, ulong commandBuffer, ulong recordSize) + { + if (recordSize == 0 || recordSize > int.MaxValue) + { + return false; + } + + Span record = stackalloc byte[(int)recordSize]; + record.Clear(); return AppendCommandBufferRecord(ctx, commandBuffer, record); } @@ -1012,30 +1442,77 @@ public static class AmprExports ulong commandBuffer, ulong equeue, ulong ident, - ulong completionToken, - ulong userData) + ulong data) { Span record = stackalloc byte[(int)KernelEventQueueRecordSize]; record.Clear(); BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType); - BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr); BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken); + BinaryPrimitives.WriteInt32LittleEndian(record[0x10..], unchecked((int)ident)); + BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], data); - return AppendCommandBufferRecord(ctx, commandBuffer, record); + if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return false; + } + + ulong recordOffset; + lock (state) + { + recordOffset = state.WriteOffset; + } + + if (!AppendCommandBufferRecord(ctx, commandBuffer, record)) + { + return false; + } + + lock (state) + { + state.KernelEventCommands.Add(new KernelEventCommand + { + RecordOffset = recordOffset, + Equeue = equeue, + Id = unchecked((int)ident), + Data = data, + }); + } + + return true; } private static bool AppendWriteAddressRecord(CpuContext ctx, ulong commandBuffer, ulong address, ulong value) { Span record = stackalloc byte[(int)WriteAddressRecordSize]; record.Clear(); - BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], WriteAddressRecordType); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], address); - BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], value); - return AppendCommandBufferRecord(ctx, commandBuffer, record); + if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null) + { + return false; + } + + ulong recordOffset; + lock (state) + { + recordOffset = state.WriteOffset; + } + + if (!AppendCommandBufferRecord(ctx, commandBuffer, record)) + { + return false; + } + + lock (state) + { + state.WriteAddressCommands.Add(new WriteAddressCommand + { + RecordOffset = recordOffset, + Address = address, + Value = value, + }); + } + + return true; } private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan record) @@ -1062,11 +1539,25 @@ public static class AmprExports state.WriteOffset += recordSize; state.CommandCount++; + if (!WriteCommandBufferProgress(ctx, commandBuffer, state)) + { + state.WriteOffset -= recordSize; + state.CommandCount--; + return false; + } } return true; } + private static bool WriteCommandBufferProgress(CpuContext ctx, ulong commandBuffer, CommandBufferState state) + { + return state.WriteOffset <= uint.MaxValue && + state.CommandCount <= uint.MaxValue && + ctx.TryWriteUInt32(commandBuffer + CommandBufferOffsetOffset, (uint)state.WriteOffset) && + ctx.TryWriteUInt32(commandBuffer + CommandBufferNumOffset, (uint)state.CommandCount); + } + private static bool CompleteKernelEventQueueRecord(CpuContext ctx, ulong recordAddress) { Span record = stackalloc byte[(int)KernelEventQueueRecordSize]; diff --git a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs index 6107e8f..62dc7a0 100644 --- a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs +++ b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs @@ -19,26 +19,43 @@ internal static class AmprFileRegistry concurrencyLevel: Math.Max(4, Environment.ProcessorCount), capacity: 1_048_576); private static readonly object _indexGate = new(); + private static readonly object _resolvedFileGate = new(); + private static readonly Dictionary _resolvedIdsByPath = new(HostFsPath.Comparer); + private static readonly ConcurrentDictionary _resolvedPathsById = new(); + private static readonly ConcurrentDictionary _ambiguousCompatibilityIds = new(); + // Resolved handles use a separate range from the 31-bit compatibility hashes. + // Never reuse a handle while queued reads can still refer to it. + private static uint _nextResolvedId = 0x80000000; private static string? _indexedApp0Root; private static string? _indexingApp0Root; private static int _preloadStarted; public static uint Register(string guestPath, string hostPath) { + var resolvedPathKey = Path.GetFullPath(hostPath); if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0) { RegisterApp0Relative(relative, hostPath); - return ComputeFileId("$/" + relative); } - - var id = ComputeFileId(guestPath); - _hostPathsById[id] = hostPath; - return id; + lock (_resolvedFileGate) + { + if (_resolvedIdsByPath.TryGetValue(resolvedPathKey, out var existingId)) + return existingId; + if (_nextResolvedId == uint.MaxValue) + throw new InvalidOperationException("APR file identifiers are exhausted."); + var resolvedId = _nextResolvedId++; + _resolvedPathsById[resolvedId] = hostPath; + _resolvedIdsByPath.Add(resolvedPathKey, resolvedId); + return resolvedId; + } } public static bool TryGetHostPath(uint id, out string hostPath) { - return _hostPathsById.TryGetValue(id, out hostPath!); + if ((id & 0x80000000) != 0) + return _resolvedPathsById.TryGetValue(id, out hostPath!); + hostPath = null!; + return !_ambiguousCompatibilityIds.ContainsKey(id) && _hostPathsById.TryGetValue(id, out hostPath!); } /// Test hook: wipe registry state between cases. @@ -47,6 +64,13 @@ internal static class AmprFileRegistry lock (_indexGate) { _hostPathsById.Clear(); + _ambiguousCompatibilityIds.Clear(); + lock (_resolvedFileGate) + { + _resolvedIdsByPath.Clear(); + _resolvedPathsById.Clear(); + _nextResolvedId = 0x80000000; + } _indexedApp0Root = null; _indexingApp0Root = null; _preloadStarted = 0; @@ -257,12 +281,19 @@ internal static class AmprFileRegistry private static void Publish(uint hash, string relative, string hostPath) { - _hostPathsById[FnvContinueUtf8(hash, relative)] = hostPath; + PublishCompatibilityPath(NormalizeFileId(FnvContinueUtf8(hash, relative)), hostPath); + } + + private static void PublishCompatibilityPath(uint id, string hostPath) + { + var existingPath = _hostPathsById.GetOrAdd(id, hostPath); + if (!HostFsPath.Comparer.Equals(existingPath, hostPath)) + _ambiguousCompatibilityIds.TryAdd(id, 0); } internal static uint ComputeFileId(string guestPath) { - return FnvContinueUtf8(OffsetBasis, guestPath); + return NormalizeFileId(FnvContinueUtf8(OffsetBasis, guestPath)); } internal static IEnumerable EnumerateApp0PathAliases(string guestPath) @@ -332,7 +363,8 @@ internal static class AmprFileRegistry // Distinct roots must not share a cache file. Folding case is only // correct where the host filesystem folds it too. var rootKey = OperatingSystem.IsWindows() ? normalizedRoot.ToLowerInvariant() : normalizedRoot; - var rootHash = ComputeFileId(rootKey); + // Keep raw hashes for cache filenames. Guest file identifiers use 31 bits. + var rootHash = FnvContinueUtf8(OffsetBasis, rootKey); return Path.Combine(cacheDir, $"app0-{rootHash:x8}.v{version}.idx"); } @@ -427,10 +459,11 @@ internal static class AmprFileRegistry ? normalizedRoot + entry.Relative.Replace('/', Path.DirectorySeparatorChar) : normalizedRoot + Path.DirectorySeparatorChar + entry.Relative.Replace('/', Path.DirectorySeparatorChar); - _hostPathsById[entry.Id0] = hostPath; - _hostPathsById[entry.Id1] = hostPath; - _hostPathsById[entry.Id2] = hostPath; - _hostPathsById[entry.Id3] = hostPath; + // Normalize cached identifiers to the guest's 31-bit range. + PublishCompatibilityPath(NormalizeFileId(entry.Id0), hostPath); + PublishCompatibilityPath(NormalizeFileId(entry.Id1), hostPath); + PublishCompatibilityPath(NormalizeFileId(entry.Id2), hostPath); + PublishCompatibilityPath(NormalizeFileId(entry.Id3), hostPath); }); } else @@ -539,12 +572,14 @@ internal static class AmprFileRegistry out uint app0, out uint bare) { - var dollar = FnvContinueUtf8( + var dollar = NormalizeFileId(FnvContinueUtf8( FnvContinueAscii(FnvContinueAscii(OffsetBasis, (byte)'$'), (byte)'/'), - relative); - app0Slash = FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "/app0/"u8), relative); - app0 = FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "app0/"u8), relative); - bare = FnvContinueUtf8(OffsetBasis, relative); + relative)); + app0Slash = NormalizeFileId( + FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "/app0/"u8), relative)); + app0 = NormalizeFileId( + FnvContinueUtf8(FnvContinueAsciiPrefix(OffsetBasis, "app0/"u8), relative)); + bare = NormalizeFileId(FnvContinueUtf8(OffsetBasis, relative)); return dollar; } @@ -571,6 +606,13 @@ internal static class AmprFileRegistry private const uint OffsetBasis = 2166136261; private const uint FnvPrime = 16777619; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint NormalizeFileId(uint hash) + { + // Keep compatibility hashes in the nonnegative signed 32-bit range. + return hash & 0x7fffffffu; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint FnvContinueAscii(uint hash, byte value) { diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs index 2f29a1b..fb34aed 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Draws.cs @@ -125,8 +125,8 @@ public sealed partial class GpuCommandInterpreter DrawIndexed(packetAddress, opcode, indexCount, indexAddress, indexedInstanceCount, unchecked((int)baseVertex), indexedStartInstance, DrawOffsetSource.IndirectArguments); } - internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) => - _host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator); + internal void DispatchDirect(uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) => + _host.DispatchDirect(SubmitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress); internal void DispatchIndirect(uint dataOffset, uint dispatchInitiator) { @@ -136,7 +136,7 @@ public sealed partial class GpuCommandInterpreter } var argumentsAddress = DispatchIndirectArgumentsBase + dataOffset; - DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator); + DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), dispatchInitiator, argumentsAddress); } internal uint DrawIndexPacket(in PacketContext packet, ReadOnlySpan payload) @@ -271,7 +271,7 @@ public sealed partial class GpuCommandInterpreter throw _host.Fatal($"The indirect dispatch arguments address is zero: address=0x{packet.PacketAddress:X16}."); } - DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2]); + DispatchDirect(ReadDword(argumentsAddress), ReadDword(argumentsAddress + 4), ReadDword(argumentsAddress + 8), payload[2], argumentsAddress); return 3; } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs index d3e01f5..d8425dc 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs @@ -119,7 +119,7 @@ public interface ICommandStreamHost void DrawAuto(ulong submitId, in DrawAutoArguments arguments); - void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator); + void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0); // Called when a queue reset packet clears the processor. void OnQueueReset(int queueId); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs index a1a4dd5..62ebbb8 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ManagedCommandStreamHost.cs @@ -221,7 +221,7 @@ public class ManagedCommandStreamHost : ICommandStreamHost { } - public virtual void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public virtual void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { } diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs index 36dfa35..ba2acf7 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ContextRegisters.cs @@ -192,6 +192,22 @@ public struct ColorControlRegisters }; } +public struct DepthRenderOverrideRegisters +{ + public bool ForceZValid; + public bool ForceZDirty; + public bool ForceStencilValid; + public bool ForceStencilDirty; + + public static DepthRenderOverrideRegisters Decode(uint value) => new() + { + ForceZValid = (value & 0x2000_0000u) != 0, + ForceZDirty = (value & 0x0800_0000u) != 0, + ForceStencilValid = (value & 0x4000_0000u) != 0, + ForceStencilDirty = (value & 0x1000_0000u) != 0, + }; +} + public struct ScanModeRegisters { public bool MultisampleEnable; @@ -348,6 +364,7 @@ public sealed class ContextRegisters public ScreenViewportRegisters ScreenViewport = new(); public ClipControlRegisters Clip; public ColorControlRegisters ColorControl = new(); + public DepthRenderOverrideRegisters DepthRenderOverride; public ScanModeRegisters ScanMode = new(); public SampleLocationRegisters SampleLocations = new(); public AntialiasingConfigRegisters AntialiasingConfig; diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs index d1b920d..8715653 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterBanks.cs @@ -57,6 +57,7 @@ public sealed class RegisterBanks public void ApplyContextState(ContextStateOperation operation) { + var previousViewport = Context.ScreenViewport.Viewports[0]; switch (operation) { case ContextStateOperation.Clear: @@ -85,6 +86,13 @@ public sealed class RegisterBanks default: throw Fatal($"The context state operation is unknown: operation={(uint)operation}."); } + if (Rendering.RenderTrace.Enabled) + { + var currentViewport = Context.ScreenViewport.Viewports[0]; + Rendering.RenderTrace.Write( + $"ContextState operation={operation} previousZScale={previousViewport.ZScale} previousZOffset={previousViewport.ZOffset} " + + $"zScale={currentViewport.ZScale} zOffset={currentViewport.ZOffset}"); + } } private void Push() diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs index 40562cb..ac716e0 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriteTable.cs @@ -26,8 +26,19 @@ public static class RegisterWriteTable RegisterWriters.FillUserConfig(UserConfig, UserConfigIndirect); } - public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => - WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true); + public static uint WriteContextPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) + { + var consumed = WritePacket(banks, in packet, offset, values, Context, ContextIndirect, "context", emptyIsHandled: true); + if (Rendering.RenderTrace.Enabled) + { + for (var index = 0; index < values.Length && index < consumed; index++) + { + TraceDepthStateWrite(offset + (uint)index, values[index], packet.PacketAddress, "packet"); + } + } + + return consumed; + } // A shader packet without values has no writer; the other banks accept it. public static uint WriteShaderPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) @@ -44,8 +55,27 @@ public static class RegisterWriteTable public static uint WriteUserConfigPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => WritePacket(banks, in packet, offset, values, UserConfig, UserConfigIndirect, "user-config", emptyIsHandled: true); - public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) => + public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) + { WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress); + if (Rendering.RenderTrace.Enabled) + { + TraceDepthStateWrite(offset, value, tableAddress, "table"); + } + } + + private static void TraceDepthStateWrite(uint offset, uint value, ulong address, string source) + { + if (offset != ContextRegisterOffset.PaClClipCntl && + offset != ContextRegisterOffset.PaClVteCntl && + offset != ContextRegisterOffset.PaClViewportXScale + 4 && + offset != ContextRegisterOffset.PaClViewportXScale + 5) + { + return; + } + + Rendering.RenderTrace.Write($"DepthStateWrite source={source} address=0x{address:X16} register=0x{offset:X4} value=0x{value:X8}"); + } public static void WriteShaderEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress) { diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs index bdd3b5b..ea46226 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Context.cs @@ -16,9 +16,10 @@ internal static partial class RegisterWriters public static void FillContext(RegisterPacketWriter?[] direct, RegisterWriter?[] indirect) { direct[DbRenderControl] = RenderControlPacket; + direct[DbRenderOverride] = DepthRenderOverridePacket; foreach (var offset in new[] { - DbCountControl, DbRenderOverride, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, + DbCountControl, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, TaBcBaseAddr, TaBcBaseAddrHi, CbDccControl, PaSuPointSize, PaSuPointMinMax, DbAlphaToMask, VgtDrawPayloadCntl, VgtPrimitiveIdReset, PaClObjPrimIdCntl, PaSuVtxCntl, PaScFovWindowLr, PaScFovWindowTb, PaScModeCntl1, PaScAaMaskX0Y0X1Y0, PaScAaMaskX0Y1X1Y1, PsShaderSampleExclusionMask, PaScBinnerCntl0, PaScBinnerCntl1, @@ -164,7 +165,7 @@ internal static partial class RegisterWriters foreach (var offset in new[] { - CbDccControl, DbCountControl, DbSResultsCompareState0, DbSResultsCompareState1, DbRenderOverride, DbRenderOverride2, + CbDccControl, DbCountControl, DbSResultsCompareState0, DbSResultsCompareState1, DbRenderOverride2, DbDfsmControl, DbRmiL2CacheControl, CbRmiGl2CacheControl, TaBcBaseAddr, TaBcBaseAddrHi, PaSuPointSize, PaSuPointMinMax, SpiTmpringSize, VgtDrawPayloadCntl, VgtPrimitiveIdReset, PaClObjPrimIdCntl, PaScFovWindowLr, PaScFovWindowTb, PaScFsrEnable, FsrRecursions0, FsrRecursions1, PaScModeCntl1, PaScAaMaskX0Y0X1Y0, PaScAaMaskX0Y1X1Y1, PaSuVtxCntl, PsShaderSampleExclusionMask, @@ -175,6 +176,7 @@ internal static partial class RegisterWriters } indirect[SpiVsOutConfig] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputConfiguration = value; + indirect[DbRenderOverride] = static (banks, _, value) => banks.Context.DepthRenderOverride = DepthRenderOverrideRegisters.Decode(value); indirect[SpiShaderPosFormat] = static (banks, _, value) => banks.Context.ShaderInterface.PositionExportFormat = value; indirect[SpiShaderIdxFormat] = static (banks, _, value) => banks.Context.ShaderInterface.IndexExportFormat = value; indirect[PaClVsOutCntl] = static (banks, _, value) => banks.Context.ShaderInterface.VertexOutputControl = value; @@ -314,6 +316,9 @@ internal static partial class RegisterWriters private static uint RenderControlPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => SingleValue(banks, in packet, offset, values, RenderControlEntry); + private static uint DepthRenderOverridePacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => + SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbRenderOverride]!); + private static uint StencilClearPacket(RegisterBanks banks, in PacketContext packet, uint offset, ReadOnlySpan values) => SingleValue(banks, in packet, offset, values, RegisterWriteTable.ContextIndirect[DbStencilClear]!); diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs index 10467df..6817845 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.Transfers.cs @@ -162,15 +162,59 @@ public sealed unsafe partial class CachedImage return new CommandBuffer(_scheduler.Current.Handle); } + private BufferImageCopy[]? SanitizeUploadCopies(ReadOnlySpan copies) + { + BufferImageCopy[]? sanitized = null; + for (var index = 0; index < copies.Length; index++) + { + var copy = copies[index]; + var mip = (int)copy.ImageSubresource.MipLevel; + var width = Math.Max(Backing.Extent.Width >> mip, 1u); + var height = Math.Max(Backing.Extent.Height >> mip, 1u); + var depth = Backing.ImageType == ImageType.Type3D + ? Math.Max(Backing.Extent.Depth >> mip, 1u) + : 1u; + if (copy.ImageOffset.X < 0 || copy.ImageOffset.Y < 0 || copy.ImageOffset.Z < 0 || + (uint)copy.ImageOffset.X >= width || (uint)copy.ImageOffset.Y >= height || + (uint)copy.ImageOffset.Z >= depth) + { + throw SubmissionScheduler.Fatal( + $"The image upload offset is outside the backing image: mip={mip} " + + $"offset={copy.ImageOffset.X},{copy.ImageOffset.Y},{copy.ImageOffset.Z} extent={width}x{height}x{depth}."); + } + + var availableWidth = width - (uint)copy.ImageOffset.X; + var availableHeight = height - (uint)copy.ImageOffset.Y; + var availableDepth = depth - (uint)copy.ImageOffset.Z; + var clamped = new Extent3D( + Math.Min(copy.ImageExtent.Width, availableWidth), + Math.Min(copy.ImageExtent.Height, availableHeight), + Math.Min(copy.ImageExtent.Depth, availableDepth)); + if (clamped.Width == copy.ImageExtent.Width && + clamped.Height == copy.ImageExtent.Height && + clamped.Depth == copy.ImageExtent.Depth) + { + continue; + } + + sanitized ??= copies.ToArray(); + sanitized[index].ImageExtent = clamped; + } + + return sanitized; + } + public void UploadFromBuffer(ReadOnlySpan copies, VkBuffer buffer, ulong offset, ulong size) { var command = BeginTransfer(copies, buffer, size); + var sanitized = SanitizeUploadCopies(copies); + var uploadCopies = sanitized is null ? copies : sanitized.AsSpan(); var bufferBarrier = BufferBarrier(buffer, offset, size, AccessFlags.MemoryWriteBit, AccessFlags.TransferReadBit); var (imageBarriers, sourceStages) = GetBarriers(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, PipelineStageFlags.TransferBit, null); RecordBarriers(command, sourceStages | PipelineStageFlags.AllCommandsBit, PipelineStageFlags.TransferBit, &bufferBarrier, imageBarriers); - fixed (BufferImageCopy* regions = copies) + fixed (BufferImageCopy* regions = uploadCopies) { - _device.Vk.CmdCopyBufferToImage(command, buffer, Backing.Handle, ImageLayout.TransferDstOptimal, (uint)copies.Length, regions); + _device.Vk.CmdCopyBufferToImage(command, buffer, Backing.Handle, ImageLayout.TransferDstOptimal, (uint)uploadCopies.Length, regions); } bufferBarrier = BufferBarrier(buffer, offset, size, AccessFlags.TransferReadBit, MemoryAccess); @@ -331,6 +375,52 @@ public sealed unsafe partial class CachedImage } } + public void CopyDepthStencilFrom(CachedImage source, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) + { + var requested = aspects & (ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit); + var available = ViewFormatRules.FullAspects(Backing.Format); + if (requested == 0 || (requested & ~available) != 0 || source.Backing.Format != Backing.Format || + source.Backing.ImageType != ImageType.Type2D || Backing.ImageType != ImageType.Type2D || + source.Backing.Samples != Backing.Samples || range.LevelCount != 1 || + range.BaseLevel >= source.Backing.MipLevels || range.BaseLevel >= Backing.MipLevels || + range.BaseLayer >= source.Backing.Layers || range.BaseLayer >= Backing.Layers || + range.LayerCount == 0 || range.LayerCount > source.Backing.Layers - range.BaseLayer || + range.LayerCount > Backing.Layers - range.BaseLayer) + { + throw SubmissionScheduler.Fatal( + $"The depth/stencil copy is invalid: aspects={(uint)aspects:X} format={(int)Backing.Format} sourceFormat={(int)source.Backing.Format} " + + $"range={range.BaseLevel}+{range.LevelCount}/{range.BaseLayer}+{range.LayerCount} samples={source.Backing.Samples}->{Backing.Samples}."); + } + + _scheduler.EndRendering(); + var command = new CommandBuffer(_scheduler.Current.Handle); + source.Transition(ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit, range, command); + Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command); + + ImageCopy* regions = stackalloc ImageCopy[2]; + uint count = 0; + if ((requested & ImageAspectFlags.DepthBit) != 0) + { + var layers = new ImageSubresourceLayers(ImageAspectFlags.DepthBit, range.BaseLevel, range.BaseLayer, range.LayerCount); + regions[count++] = new ImageCopy { SrcSubresource = layers, DstSubresource = layers, Extent = extent }; + } + + if ((requested & ImageAspectFlags.StencilBit) != 0) + { + var layers = new ImageSubresourceLayers(ImageAspectFlags.StencilBit, range.BaseLevel, range.BaseLayer, range.LayerCount); + regions[count++] = new ImageCopy { SrcSubresource = layers, DstSubresource = layers, Extent = extent }; + } + + _device.Vk.CmdCopyImage( + command, + source.Backing.Handle, + ImageLayout.TransferSrcOptimal, + Backing.Handle, + ImageLayout.TransferDstOptimal, + count, + regions); + } + public static uint CalculateRowsPerCopy(ulong rowSize, uint rows, ulong capacity) { if (rowSize == 0 || rows == 0 || rowSize > capacity) diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs index 6b28566..d4a2de9 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs @@ -228,6 +228,15 @@ public sealed unsafe partial class CachedImage : IDisposable { flags |= ImageCreateFlags.Create2DArrayCompatibleBit; } + else if (description.Type == GuestImageType.Color2D && + description.Samples == 1 && + description.Extent.Depth == 1 && + description.Extent.Width == description.Extent.Height && + description.Resources.Layers >= 6 && + description.Resources.Layers % 6 == 0) + { + flags |= ImageCreateFlags.CreateCubeCompatibleBit; + } return flags; } diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs index 388457a..0b31a83 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Overlap.cs @@ -254,6 +254,60 @@ public sealed partial class GuestImageCache } } + private static bool GrowImageToLayers(ref ImageDescription info, uint layers) + { + var current = info.Resources.Layers; + if (info.Resources.Levels != 1 || current == 0 || layers <= current || + info.MipLayout[0].Offset != 0 || info.MipLayout[0].Size != info.Data.Size) + { + return false; + } + + static bool TryStretch(ref GuestSpan range, uint currentLayers, uint newLayers) + { + if (range.Size == 0) + { + return true; + } + + if (range.Size % currentLayers != 0) + { + return false; + } + + range = new GuestSpan(range.Address, range.Size / currentLayers * newLayers); + return true; + } + + var grown = info; + if (!TryStretch(ref grown.Data, current, layers) || + !TryStretch(ref grown.Stencil, current, layers) || + !TryStretch(ref grown.Metadata.Range, current, layers)) + { + return false; + } + + grown.MipLayout[0].Size = grown.Data.Size; + grown.Resources = grown.Resources with { Layers = layers }; + info = grown; + return true; + } + + private static bool SameMipLayout(in ImageDescription left, in ImageDescription right) + { + for (var level = 0; level < ImageDescription.MaxLevels; level++) + { + var a = left.MipLayout[level]; + var b = right.MipLayout[level]; + if (a.Offset != b.Offset || a.Size != b.Size || a.Pitch != b.Pitch || a.Height != b.Height) + { + return false; + } + } + + return true; + } + private ResourceSlotIdentifier ResolveDepthOverlap(in ImageDescription requested, ImageRole role, ResourceSlotIdentifier cachedImageIdentifier) { using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageOverlap); @@ -322,7 +376,13 @@ public sealed partial class GuestImageCache } else { - replacementInfo.Resources = SubresourceCount.Max(requested.Resources, cachedInfo.Resources); + var merged = SubresourceCount.Max(requested.Resources, cachedInfo.Resources); + if (merged.Layers > requested.Resources.Layers && !GrowImageToLayers(ref replacementInfo, merged.Layers)) + { + merged = merged with { Layers = requested.Resources.Layers }; + } + + replacementInfo.Resources = merged; } replacementInfo.HtileClearMask = 0; @@ -413,7 +473,8 @@ public sealed partial class GuestImageCache return new OverlapResolution(GrowImage(requested, cachedImageIdentifier)); } - if (requested.TileMode != cachedInfo.TileMode) + if (requested.TileMode != cachedInfo.TileMode || + (requested.Resources == cachedInfo.Resources && !SameMipLayout(requested, cachedInfo))) { if (safeToDelete) { diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs index 81f4a76..e64d19a 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Transfers.cs @@ -108,6 +108,15 @@ public sealed unsafe partial class GuestImageCache plan.Layout = TextureTransferLayout.Compute(format, info.Extent.Width, info.Extent.Height, info.Resources.Levels, layers, info.TileMode, info.Data.Size, allowDepthTile, volume, owner); plan.Regions = plan.Layout.BuildCopies(); + if (info.IsDepth) + { + for (var index = 0; index < plan.Regions.Count; index++) + { + var region = plan.Regions[index]; + region.ImageSubresource.AspectMask = ImageAspectFlags.DepthBit; + plan.Regions[index] = region; + } + } plan.Tiled = plan.Layout.Surface.Description.TileMode != GuestTileMode.Linear; if (plan.Tiled) { diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs index f59fad7..816d7fc 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.ColorTarget.cs @@ -69,6 +69,10 @@ public static partial class ImageRequestBuilders if (words.BaseAddress == 0 || mask == 0) { + if (Rendering.RenderTrace.Enabled) + { + Rendering.RenderTrace.Write($"ColorAttachmentRejected reason={(words.BaseAddress == 0 ? "zero-address" : "masked-output")} address=0x{words.BaseAddress:X16} mask=0x{mask:X}"); + } return null; } diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs index acca197..05eab54 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs @@ -61,16 +61,33 @@ public static partial class ImageRequestBuilders private static bool HtileStencilCompatible(bool hasStencil, bool hasHtile, bool htileStencilDisabled) => !hasStencil || !hasHtile || htileStencilDisabled; // Builds the request for the bound depth target. Null when no depth or stencil state is active. - public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) + public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) => + DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false); + + // Builds the source or destination image used by DB_RENDER_OVERRIDE depth/stencil copies. + public static DepthTargetResolution? DepthTargetCopy(in DepthTargetWords depthWords, IImageFormatSupport device, bool writeBuffer) => + DepthTargetCore(in depthWords, device, copyMode: true, writeBuffer); + + private static DepthTargetResolution? DepthTargetCore(in DepthTargetWords depthWords, IImageFormatSupport device, bool copyMode, bool writeBuffer) { var hasStencil = depthWords.StencilFormat != GuestStencilFormat.Invalid; - var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor; - var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor); - if (!depthActive && !stencilActive) + if (!copyMode) { - return null; + var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor; + var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor); + if (!depthActive && !stencilActive) + { + if (Rendering.RenderTrace.Enabled) + { + Rendering.RenderTrace.Write("DepthAttachmentRejected reason=depth-and-stencil-inactive"); + } + return null; + } } + var depthAddress = copyMode && writeBuffer ? depthWords.ZWriteBase : depthWords.ZReadBase; + var stencilAddress = copyMode && writeBuffer ? depthWords.StencilWriteBase : depthWords.StencilReadBase; + // The size register is independent state; a zero encoding alone must not produce an attachment. var attachmentUnbound = depthWords.DepthFormat == GuestDepthFormat.Invalid && !hasStencil && depthWords.SamplesLog2 == 0 && !depthWords.ZExpClear && !depthWords.ZPartiallyResident && depthWords.MaxMip == 0 && @@ -79,6 +96,10 @@ public static partial class ImageRequestBuilders depthWords.StencilWriteBase == 0 && depthWords.HtileBase == 0 && !depthWords.HtileAcceleration && depthWords.ShadingRateEncoding == 0 && depthWords.XMax == 0 && depthWords.YMax == 0; if (attachmentUnbound) { + if (Rendering.RenderTrace.Enabled) + { + Rendering.RenderTrace.Write("DepthAttachmentRejected reason=unbound-register-state"); + } return null; } @@ -97,7 +118,7 @@ public static partial class ImageRequestBuilders if (depthWords.CopyDepthToColor || depthWords.CopyStencilToColor || depthWords.CopyCentroid || depthWords.CopySample != 0 || depthWords.ZExpClear || depthWords.StencilExpClear || depthWords.ZPartiallyResident || depthWords.StencilPartiallyResident || depthWords.MaxMip != 0 || depthWords.ViewMipLevel != 0 || unsupportedShadingRate || - depthWords.ZReadBase == 0 || (!depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthWords.ZReadBase & 0xFFFF) != 0 || depthWords.DepthCompare > 7) + depthAddress == 0 || (!copyMode && !depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthAddress & 0xFFFF) != 0 || depthWords.DepthCompare > 7) { throw SubmissionScheduler.Fatal( $"The depth register state is not supported: zInfo=0x{depthWords.ZInfo:X8} stencilInfo=0x{depthWords.StencilInfo:X8} view=0x{depthWords.DepthView:X8} " + @@ -107,13 +128,13 @@ public static partial class ImageRequestBuilders if (hasStencil) { if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) || - depthWords.StencilReadBase == 0 || (!depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (depthWords.StencilReadBase & 0xFFFF) != 0) + stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0) { throw SubmissionScheduler.Fatal( $"The stencil attachment state is not supported: stencilInfo=0x{depthWords.StencilInfo:X8} htile={hasHtile} read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}."); } } - else if (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0) + else if (!copyMode && (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0)) { throw SubmissionScheduler.Fatal($"Stencil state is set without an active stencil attachment: read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}."); } @@ -169,20 +190,20 @@ public static partial class ImageRequestBuilders var depthBackingSize = (ulong)depthSize.Size * view.ImageLayers; var stencilBackingSize = (ulong)stencilSize.Size * view.ImageLayers; var htileBackingSize = (ulong)htileSize.Size * view.ImageLayers; - if (!new GuestSpan(depthWords.ZReadBase, depthBackingSize).IsValid || - (hasStencil && !new GuestSpan(depthWords.StencilReadBase, stencilBackingSize).IsValid) || + if (!new GuestSpan(depthAddress, depthBackingSize).IsValid || + (hasStencil && !new GuestSpan(stencilAddress, stencilBackingSize).IsValid) || (hasHtile && !new GuestSpan(depthWords.HtileBase, htileBackingSize).IsValid)) { throw SubmissionScheduler.Fatal( - $"The layered depth backing range is invalid: depth=0x{depthWords.ZReadBase:X16}+0x{depthBackingSize:X} stencil=0x{depthWords.StencilReadBase:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}."); + $"The layered depth backing range is invalid: depth=0x{depthAddress:X16}+0x{depthBackingSize:X} stencil=0x{stencilAddress:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}."); } - var stencilAddress = hasStencil ? depthWords.StencilReadBase : 0; + stencilAddress = hasStencil ? stencilAddress : 0; stencilBackingSize = hasStencil ? stencilBackingSize : 0; var htileAddress = hasHtile ? depthWords.HtileBase : 0; htileBackingSize = hasHtile ? htileBackingSize : 0; var description = ImageDescription.Create(); - description.Data = new GuestSpan(depthWords.ZReadBase, depthBackingSize); + description.Data = new GuestSpan(depthAddress, depthBackingSize); description.Stencil = new GuestSpan(stencilAddress, stencilBackingSize); description.PixelFormat = format; description.GuestFormat = rule.GuestFormat; @@ -203,7 +224,7 @@ public static partial class ImageRequestBuilders var request = new ImageRequest(description, viewDescription, ImageRole.DepthTarget); return new DepthTargetResolution( request, format, width, height, samples, hasStencil, hasHtile, - depthWords.ZReadBase, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize, + depthAddress, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize, depthWords.DepthClearEnabled, hasStencil && depthWords.StencilClearEnabled && !depthWords.StencilWriteDisabled, depthWords.DepthWriteDisabled, depthWords.StencilWriteDisabled); } diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs index a9d7666..4d268ba 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs @@ -14,8 +14,10 @@ public readonly record struct TextureRequestResolution(ImageRequest Request, boo public static partial class ImageRequestBuilders { // A null descriptor binds a one-texel image of the numeric class the shader expects. - public static ImageRequest NullTexture(TextureNumericClass numericClass, bool storage) + public static ImageRequest NullTexture(in ShaderImageShape shape) { + var numericClass = shape.NumericClass; + var storage = shape.Storage; var (format, guestFormat) = numericClass switch { TextureNumericClass.Float => (Format.R32Sfloat, GuestPixelFormat.Bits32Float), @@ -26,16 +28,22 @@ public static partial class ImageRequestBuilders var description = ImageDescription.Create(); description.PixelFormat = format; description.GuestFormat = guestFormat; - description.Type = GuestImageType.Color2D; + description.Type = shape.Volume + ? GuestImageType.Color3D + : shape.OneDimensional ? GuestImageType.Color1D : GuestImageType.Color2D; description.Extent = new Extent3D(1, 1, 1); description.Resources = SubresourceCount.Single; description.BytesPerBlock = 4; - description.Samples = 1; + description.Samples = shape.Multisampled ? 4u : 1u; description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 0, Pitch = 1, Height = 1 }; var view = ImageViewDescription.Default with { Format = format, - Type = ImageViewType.Type2D, + Type = shape.Volume + ? ImageViewType.Type3D + : shape.OneDimensional + ? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D + : shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D, Aspect = ImageAspectFlags.ColorBit, Usage = storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit, }; @@ -104,8 +112,10 @@ public static partial class ImageRequestBuilders throw SubmissionScheduler.Fatal($"The texture base layer is outside the image: baseLayer={baseLayer} layers={imageLayers} address=0x{descriptor.BaseAddress:X16}."); } - var type = shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D; var layerCount = shape.Arrayed ? imageLayers - baseLayer : 1; + var type = shape.OneDimensional + ? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D + : shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D; return new ImageViewDescription(format, type, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, baseLayer, layerCount, mapping, usage); } @@ -115,8 +125,12 @@ public static partial class ImageRequestBuilders // Cube maps become two-dimensional arrays; the multisample kinds keep their own type. private static GuestImageType TextureType(GuestImageType type) => type == GuestImageType.Cube ? GuestImageType.Color2DArray : type; - // Every non-volume texture is a two-dimensional image; the translator compiles 1D textures as 2D. - private static GuestImageType TextureBaseType(GuestImageType type) => type == GuestImageType.Color3D ? GuestImageType.Color3D : GuestImageType.Color2D; + private static GuestImageType TextureBaseType(GuestImageType type) => type switch + { + GuestImageType.Color1DArray => GuestImageType.Color1D, + GuestImageType.Color2DArray or GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray => GuestImageType.Color2D, + _ => type, + }; private static bool IsMultisampledTexture(GuestImageType type) => type is GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray; @@ -126,11 +140,10 @@ public static partial class ImageRequestBuilders Span padded = stackalloc uint[8]; words[..Math.Min(words.Length, 8)].CopyTo(padded); var descriptor = new TextureDescriptorWords(padded); - var compactDescriptor = words.Length < 8; var storage = shape.Storage; if (descriptor.BaseAddress == 0) { - var nullRequest = NullTexture(shape.NumericClass, storage); + var nullRequest = NullTexture(shape); return new TextureRequestResolution(nullRequest, false, nullRequest.View.Format, 0); } @@ -141,7 +154,7 @@ public static partial class ImageRequestBuilders var lastLevel = descriptor.LastLevel; var type = TextureType(descriptor.Type); var multisampled = IsMultisampledTexture(type); - var maxMip = compactDescriptor ? lastLevel : descriptor.MaxMip; + var maxMip = shape.R128 ? lastLevel : descriptor.MaxMip; var levels = multisampled ? 1 : maxMip + 1; var dynamicStorage = storage && shape.DynamicMip; var viewLastLevel = !multisampled && !dynamicStorage ? Math.Min(lastLevel, maxMip) : lastLevel; @@ -168,6 +181,13 @@ public static partial class ImageRequestBuilders var volume = type == GuestImageType.Color3D; var layered = type is GuestImageType.Color1DArray or GuestImageType.Color2DArray or GuestImageType.Color2DMsaaArray; var imageLayers = layered ? depth : 1; + if (shape.Cube && + (volume || multisampled || width != height || descriptor.BaseArray > descriptor.Depth || + (descriptor.Depth - descriptor.BaseArray + 1) % 6 != 0)) + { + throw SubmissionScheduler.Fatal( + $"The cubemap view is invalid: address=0x{address:X16} extent={width}x{height} layers={imageLayers} baseArray={descriptor.BaseArray} samples={samples}."); + } uint pitch; TileSizeAndAlignment size; if (multisampled) @@ -195,6 +215,11 @@ public static partial class ImageRequestBuilders } var pixelFormat = surfaceFormat.HostFormat; + // Comparison sampling needs a depth image, even when no depth target created it. + if (shape.DepthCompare && DepthFormatRule.FindByGuestFormat(format) is { } depthFormat) + { + pixelFormat = depthFormat.DepthAttachmentFormat; + } var storageViewFormat = storage && format == GuestPixelFormat.Bits32SInt ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat); var viewFormat = storage && storageViewFormat != Format.Undefined ? storageViewFormat : pixelFormat; var blockBytes = GuestPixelFormats.BlockCompressedBytes(format); diff --git a/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs b/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs index f75f3da..592d979 100644 --- a/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs +++ b/src/SharpEmu.Libs/Gpu/Images/TargetRegisterWords.cs @@ -106,4 +106,10 @@ public readonly record struct DisplaySurfaceWords( bool Compressed); // The shape the shader module was compiled with; the view type must match it. -public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Storage, bool DynamicMip, TextureNumericClass NumericClass); +public readonly record struct ShaderImageShape(bool Volume, bool Arrayed, bool Cube, bool Storage, bool DynamicMip, TextureNumericClass NumericClass, bool OneDimensional = false, bool R128 = false, bool Multisampled = false, bool DepthCompare = false) +{ + public ShaderImageShape(bool volume, bool arrayed, bool storage, bool dynamicMip, TextureNumericClass numericClass) + : this(volume, arrayed, false, storage, dynamicMip, numericClass, false, false, false) + { + } +} diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs index 612a46d..c84cc52 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalCommandStreamHost.Rendering.cs @@ -1147,6 +1147,14 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel _ = _snapshots.TrySubmitGuestImageBlit(from, to); } + bool IRenderHost.TryDispatchIndirect(ulong argumentsAddress) => false; + + void IRenderHost.CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) + { + _ = (source, destination, range, extent, aspects); + throw Fatal("Depth/stencil image copies are not implemented by the Metal command stream host."); + } + bool IRenderHost.IsMetadata(ulong address) { _ = address; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index e737b82..8fd5bc4 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -333,22 +333,38 @@ internal sealed class ShaderProgramCache private static ShaderVertexInput[] BuildVertexInputs(EmbeddedVertexFetchPlan fetch, VertexInputInfo info, ShaderSource source) { var inputs = new List(); - var aliases = new Dictionary>(); + var inputByLocation = new Dictionary(); foreach (var load in fetch.Loads) { - if (aliases.TryGetValue(load.AttributeId, out var aliasPcs)) + var location = -1; + if ((uint)load.AttributeId < (uint)info.Attributes.Length && + info.Attributes[load.AttributeId].AttributeId == load.AttributeId) { - aliasPcs.Add(load.Pc); - continue; + location = load.AttributeId; } - var location = -1; - for (var index = 0; index < info.Attributes.Length; index++) + if (location < 0) { - if (info.Attributes[index].AttributeId == load.AttributeId) + for (var index = 0; index < info.Attributes.Length; index++) { - location = index; - break; + if (info.Attributes[index].AttributeId == load.AttributeId && + info.Attributes[index].RegisterCount >= load.Components) + { + location = index; + break; + } + } + } + + if (location < 0) + { + for (var index = 0; index < info.Attributes.Length; index++) + { + if (info.Attributes[index].AttributeId == load.AttributeId) + { + location = index; + break; + } } } @@ -365,9 +381,55 @@ internal sealed class ShaderProgramCache numberFormat = decodedNumberFormat; } - aliasPcs = []; - aliases.Add(load.AttributeId, aliasPcs); - inputs.Add(new ShaderVertexInput(load.Pc, (uint)location, load.Components, numberFormat, attribute.FetchIndex != 0, aliasPcs)); + var requiredComponents = 0u; + var destinationSelect = attribute.Descriptor.DestinationSelectXYZW; + for (uint component = 0; component < load.Components; component++) + { + var selector = (destinationSelect >> (int)(component * 3)) & 0x7u; + if (selector is >= 4u and <= 7u) + { + requiredComponents = Math.Max(requiredComponents, selector - 3u); + continue; + } + + if (selector is 0u or 1u) + { + continue; + } + + throw SubmissionScheduler.Fatal( + $"The vertex program uses an unsupported attribute destination selector: hash=0x{source.Hash:X16} shader=0x{source.Address:X16} attribute={load.AttributeId} pc=0x{load.Pc:X} component={component} selector={selector}."); + } + + if (requiredComponents == 0) + { + throw SubmissionScheduler.Fatal( + $"The vertex program fetches an attribute without a memory component: hash=0x{source.Hash:X16} shader=0x{source.Address:X16} attribute={load.AttributeId} pc=0x{load.Pc:X}."); + } + + if (inputByLocation.TryGetValue(location, out var existingIndex)) + { + var existing = inputs[existingIndex]; + ((List)existing.AliasPcs).Add(load.Pc); + inputs[existingIndex] = existing with + { + FetchComponentCount = Math.Max(existing.FetchComponentCount, load.Components), + ComponentCount = Math.Max(existing.ComponentCount, requiredComponents), + }; + continue; + } + + var aliasPcs = new List(); + inputByLocation.Add(location, inputs.Count); + inputs.Add(new ShaderVertexInput( + load.Pc, + (uint)location, + load.Components, + requiredComponents, + numberFormat, + destinationSelect, + attribute.FetchIndex != 0, + aliasPcs)); } return inputs.ToArray(); @@ -470,6 +532,15 @@ internal sealed class ShaderProgramCache EnableGraphicsSubgroupOperations = enableGraphicsSubgroups, RequiredVertexOutputCount = options.RequiredVertexOutputCount, VertexInputs = entry.VertexInputs, + PositionExportControl = info.PositionExportControl, + ClipSpace = new ShaderClipSpaceTransform( + info.ClipSpace.Enabled, + info.ClipSpace.ScaleX, + info.ClipSpace.ScaleY, + info.ClipSpace.OffsetX, + info.ClipSpace.OffsetY, + info.ClipSpace.HalfExtentX, + info.ClipSpace.HalfExtentY), }; } @@ -486,6 +557,7 @@ internal sealed class ShaderProgramCache EnableGraphicsSubgroupOperations = enableGraphicsSubgroups, PixelOutputs = options.PixelOutputs, PixelInputEnable = options.PixelInputEnable, + PixelCustomInterpolationMask = info.CustomInterpolationMask, PixelInputAddress = options.PixelInputAddress, PixelInputCntl = interpolators, }; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs index 3e744af..24e532c 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/VertexInputResolver.cs @@ -16,7 +16,6 @@ public sealed class VertexTableMetadata public int VertexAttributeRegister { get; init; } = NoRegister; public uint[] InputSemantics { get; init; } = []; } - // One packed input semantic word of a shader header. public readonly record struct ShaderInputSemantic(uint Word) { @@ -77,7 +76,11 @@ public static class VertexInputResolver private const uint DirectResourceCount = DirectResourceVertexAttributeTable + 1; private const ulong DirectResourceOffsetField = 0x00; private const ulong DirectResourceCountField = 0x2C; - private const uint IdentityDestinationSelect = 4 | (5 << 3) | (6 << 6) | (7 << 9); + private static uint DestinationSelectForChannels(uint channels) => + 4u | + ((channels > 1 ? 5u : 0u) << 3) | + ((channels > 2 ? 6u : 0u) << 6) | + ((channels > 3 ? 7u : 1u) << 9); private sealed class ResolvedBuffer { @@ -259,10 +262,13 @@ public static class VertexInputResolver var descriptor = BufferDescriptorWords.From(descriptorWords); if (format != 0) { - var bufferFormat = VertexAttributeFormat.ToBufferFormat(format); + var bufferFormat = format >> 2; + var channels = (format & 0x3u) + 1u; descriptor = descriptor with { - Word3 = (descriptor.Word3 & ~((0x7Fu << 12) | 0xFFFu)) | ((bufferFormat & 0x7Fu) << 12) | IdentityDestinationSelect, + Word3 = (descriptor.Word3 & ~((0x7Fu << 12) | 0xFFFu)) | + ((bufferFormat & 0x7Fu) << 12) | + DestinationSelectForChannels(channels), }; } @@ -365,30 +371,3 @@ public static class VertexInputResolver return result; } } - -// The vertex attribute formats of the guest tables and the buffer formats they select. -public static class VertexAttributeFormat -{ - private static readonly Dictionary _bufferFormats = new() - { - [0] = 0, - [4] = 1, [8] = 2, [12] = 3, [16] = 4, [20] = 5, [24] = 6, - [28] = 7, [32] = 8, [36] = 9, [40] = 10, [44] = 11, [48] = 12, [52] = 13, - [57] = 14, [61] = 15, [65] = 16, [69] = 17, [73] = 18, [77] = 19, - [80] = 20, [84] = 21, [88] = 22, - [93] = 23, [97] = 24, [101] = 25, [105] = 26, [109] = 27, [113] = 28, [117] = 29, - [122] = 30, [126] = 31, [130] = 32, [134] = 33, [138] = 34, [142] = 35, [146] = 36, - [150] = 37, [154] = 38, [158] = 39, [162] = 40, [166] = 41, [170] = 42, [174] = 43, - [179] = 44, [183] = 45, [187] = 46, [191] = 47, [195] = 48, [199] = 49, - [203] = 50, [207] = 51, [211] = 52, [215] = 53, [219] = 54, [223] = 55, - [227] = 56, [231] = 57, [235] = 58, [239] = 59, [243] = 60, [247] = 61, - [249] = 62, [253] = 63, [257] = 64, - [263] = 65, [267] = 66, [271] = 67, [275] = 68, [279] = 69, [283] = 70, [287] = 71, - [290] = 72, [294] = 73, [298] = 74, - [303] = 75, [307] = 76, [311] = 77, - }; - - // An unknown attribute format is kept as the buffer format value it names. - public static uint ToBufferFormat(uint attributeFormat) => - _bufferFormats.TryGetValue(attributeFormat, out var bufferFormat) ? bufferFormat : attributeFormat; -} diff --git a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs index 0d70dc5..18dfd63 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/IRenderHost.cs @@ -145,6 +145,10 @@ public interface IRenderHost void Dispatch(uint groupsX, uint groupsY, uint groupsZ); + // Executes a dispatch from three uint32 group counts in guest memory when the backend can + // consume the guest buffer directly. Backends without native indirect dispatch return false. + bool TryDispatchIndirect(ulong argumentsAddress); + // Orders the buffer writes of the given shader stages before every later access. void ShaderWriteBarrier(PipelineStageFlags sourceStages); @@ -162,6 +166,8 @@ public interface IRenderHost void ResolveImage(ResourceSlotIdentifier source, uint sourceMip, uint sourceLayer, ResourceSlotIdentifier destination, uint destinationMip, uint destinationLayer); + void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects); + bool IsMetadata(ulong address); bool ClearMetadata(ulong address); diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs index 209b7a5..53145d2 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Adaptations.cs @@ -31,12 +31,14 @@ public sealed partial class RenderExecutor if (programs.SolidClear is { } clear && state.ColorCount != 0) { + TraceDrawDisposition(banks, in draw, "color-clear"); _host.ClearColorTargets(BoundColors(ref state), clear); return false; } if (IsMetadataClearQuad(banks, in draw, ref state)) { + TraceDrawDisposition(banks, in draw, "metadata-clear-quad"); return false; } @@ -44,12 +46,46 @@ public sealed partial class RenderExecutor programs.PixelInput.Stage.Program is { Images.Length: > 0 } pixelProgram && !WritesStorageImage(pixelProgram) && _host.TryRetainTargetlessDraw(banks, programs, in arguments)) { + TraceTargetlessAttachments(banks, arguments.SubmitId); + TraceDrawDisposition(banks, in draw, "retained-targetless-draw"); return false; } return true; } + private static void TraceDrawDisposition(RegisterBanks banks, in DrawCall draw, string reason) + { + if (!RenderTrace.Enabled) + { + return; + } + + ref readonly var viewport = ref banks.Context.ScreenViewport.Viewports[0]; + RenderTrace.Write( + $"DrawDisposition name={draw.Name} reason={reason} count={draw.Count} " + + $"export=0x{banks.Shader.Vertex.ExportAddress:X16} pixel=0x{banks.Shader.Pixel.Address:X16} " + + $"colorMode={banks.Context.ColorControl.Mode} targetMask=0x{banks.Context.RenderTargetMask:X8} " + + $"zScale={viewport.ZScale} zOffset={viewport.ZOffset}"); + } + + private static void TraceTargetlessAttachments(RegisterBanks banks, ulong submitId) + { + if (!RenderTrace.Enabled) + { + return; + } + + var context = banks.Context; + RenderTrace.Write($"TargetlessDepth submit={submitId} registers={context.DepthTarget}"); + for (var slot = 0; slot < ContextRegisters.ColorTargetCount; slot++) + { + RenderTrace.Write( + $"TargetlessColor submit={submitId} slot={slot} mask=0x{context.RenderTargetMaskForSlot((uint)slot):X} " + + $"registers={context.ColorTargets[slot]}"); + } + } + private static bool WritesStorageImage(ShaderProgramInfo program) { foreach (var image in program.Images) diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs index 9832906..c32885e 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs @@ -21,7 +21,7 @@ public sealed partial class RenderExecutor private const uint ImageClearStride = 16; private const uint ImageClearUserDataCount = 8; - public void Dispatch(ulong submitId, RegisterBanks banks, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public void Dispatch(ulong submitId, RegisterBanks banks, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { if (!_host.IsRecording) { @@ -76,13 +76,13 @@ public sealed partial class RenderExecutor $"buffers={program.Buffers.Length} images={program.Images.Length} samplers={program.SamplerCount}"); } - if (TryConsumeMetadataClear(input)) + if (indirectArgumentsAddress == 0 && TryConsumeMetadataClear(input)) { _host.ResetBindings(); return; } - if (TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator)) + if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator)) { _host.ResetBindings(); return; @@ -90,6 +90,9 @@ public sealed partial class RenderExecutor if (useThreadDimensions) { + // The indirect buffer carries thread counts in this mode, while Vulkan indirect + // dispatch consumes workgroup counts. Use the CPU-resolved counts after conversion. + indirectArgumentsAddress = 0; var threadsX = groupsX; var threadsY = groupsY; var threadsZ = groupsZ; @@ -104,7 +107,7 @@ public sealed partial class RenderExecutor } } - if (groupsX == 0 || groupsY == 0 || groupsZ == 0) + if (indirectArgumentsAddress == 0 && (groupsX == 0 || groupsY == 0 || groupsZ == 0)) { if (RenderTrace.Enabled && RenderTrace.ZeroDispatch()) { @@ -140,7 +143,10 @@ public sealed partial class RenderExecutor } _host.BindPipeline(PipelineBindPoint.Compute, in pipeline); - _host.Dispatch(groupsX, groupsY, groupsZ); + if (indirectArgumentsAddress == 0 || !_host.TryDispatchIndirect(indirectArgumentsAddress)) + { + _host.Dispatch(groupsX, groupsY, groupsZ); + } _host.ShaderAccessBarrier(); } diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs index b813ae4..cc31c82 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs @@ -117,7 +117,7 @@ public sealed partial class RenderExecutor viewportY, viewportWidth, viewportHeight, - viewport.ZOffset, + viewport.ZOffset - (context.Clip.DirectXClipSpace ? 0f : viewport.ZScale), viewport.ZScale + viewport.ZOffset, scissor, lineWidth, diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs index 4e16231..c8029b8 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Targets.cs @@ -20,6 +20,7 @@ public sealed partial class RenderExecutor var context = banks.Context; if (TryResolveMultisampleColor(context)) { + TraceDrawDisposition(banks, in draw, "multisample-color-resolve"); return false; } @@ -52,6 +53,7 @@ public sealed partial class RenderExecutor state.PixelActive = HasActivePixelShader(banks); if (state.ColorCount == 0 && !state.Depth.HasTarget && !state.PixelActive) { + TraceDrawDisposition(banks, in draw, "no-framebuffer"); if (RenderTrace.Enabled && RenderTrace.FramebufferSkip()) { RenderTrace.Write( @@ -97,6 +99,63 @@ public sealed partial class RenderExecutor return true; } + // DB_RENDER_OVERRIDE can turn a draw packet into a depth/stencil read-to-write copy. + private bool TryDepthStencilCopy(ContextRegisters context) + { + if (context.ColorControl.Mode != 0) + { + return false; + } + + ref readonly var words = ref context.DepthTarget; + var renderOverride = context.DepthRenderOverride; + var depthCopy = + renderOverride.ForceZDirty && + renderOverride.ForceZValid && + words.DepthFormat != GuestDepthFormat.Invalid && + words.ZReadBase != 0 && + words.ZWriteBase != 0 && + words.ZReadBase != words.ZWriteBase; + var stencilCopy = + renderOverride.ForceStencilDirty && + renderOverride.ForceStencilValid && + words.StencilFormat != GuestStencilFormat.Invalid && + words.StencilReadBase != 0 && + words.StencilWriteBase != 0 && + words.StencilReadBase != words.StencilWriteBase; + if (!depthCopy && !stencilCopy) + { + return false; + } + + var read = ImageRequestBuilders.DepthTargetCopy(in words, _host.FormatSupport, writeBuffer: false) + ?? throw _host.Fatal("A depth/stencil copy has no readable depth target."); + var write = ImageRequestBuilders.DepthTargetCopy(in words, _host.FormatSupport, writeBuffer: true) + ?? throw _host.Fatal("A depth/stencil copy has no writable depth target."); + var readRequest = read.Request; + var writeRequest = write.Request; + var source = _host.FindImage(ref readRequest, exactFormat: true); + var destination = _host.FindImage(ref writeRequest, exactFormat: true); + _host.BindRenderTarget(source); + _host.BindRenderTarget(destination); + if (source == destination || read.Format != write.Format) + { + throw _host.Fatal( + $"A depth/stencil copy must use distinct images with the same format: source={source.Index} destination={destination.Index} readFormat={(int)read.Format} writeFormat={(int)write.Format}."); + } + + var range = new SubresourceRange( + readRequest.View.BaseLevel, + readRequest.View.LevelCount, + readRequest.View.BaseLayer, + readRequest.View.LayerCount); + var extent = new Extent3D(write.Width, write.Height, 1); + var aspects = (depthCopy ? ImageAspectFlags.DepthBit : 0) | (stencilCopy ? ImageAspectFlags.StencilBit : 0); + _host.MarkGpuWritten(destination); + _host.CopyDepthStencilImage(source, destination, in range, in extent, aspects); + return true; + } + private ColorTargetState? ResolveMultisampleSlot(ContextRegisters context, uint slot) { if (ColorTargetResolver.Resolve(context, slot, DrawLayerOffset, ignoreTargetMask: true, out var resolvedSlot) is not { } resolution) diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs index 4c0561f..b392b9d 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.cs @@ -106,7 +106,7 @@ public sealed partial class RenderExecutor return; } - if (ConsumesColorMetadataOperation(banks.Context)) + if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context)) { _host.ResetBindings(); return; @@ -226,7 +226,7 @@ public sealed partial class RenderExecutor return; } - if (ConsumesColorMetadataOperation(banks.Context)) + if (ConsumesColorMetadataOperation(banks.Context) || TryDepthStencilCopy(banks.Context)) { _host.ResetBindings(); return; @@ -255,6 +255,7 @@ public sealed partial class RenderExecutor if (!ResolveTopology(userConfig, autoDraw: true, out var topology)) { + TraceDrawDisposition(banks, in draw, "no-primitive-topology"); _host.ResetBindings(); return; } @@ -588,11 +589,17 @@ public sealed partial class RenderExecutor var colorWidth = state.ColorCount != 0 ? state.Colors[0].Resolution.Extent.Width : 0; var colorHeight = state.ColorCount != 0 ? state.Colors[0].Resolution.Extent.Height : 0; var depth = state.Depth.HasTarget ? state.Depth.Target : default; + ref readonly var viewport = ref banks.Context.ScreenViewport.Viewports[0]; + var clip = banks.Context.Clip; RenderTrace.Write( $"DrawState seq={RenderTrace.NextSequence()} submit={submitId} name={draw.Name} export=0x{shader.Vertex.ExportAddress:X16} " + $"pixel=0x{shader.Pixel.Address:X16} pixelActive={state.PixelActive} count={draw.Count} instances={draw.InstanceCount} " + $"colors={state.ColorCount} color=0x{colorAddress:X10}:{colorWidth}x{colorHeight} targetMask=0x{banks.Context.RenderTargetMask:X8} " + $"depth=0x{depth.Target.DepthAddress:X10}:{depth.Target.Width}x{depth.Target.Height}:{(int)depth.Target.Format} " + - $"depthState={(depth.State.DepthTestEnabled ? 1 : 0)}/{(depth.State.DepthWriteEnabled ? 1 : 0)}/{(int)depth.State.DepthCompare}"); + $"depthState={(depth.State.DepthTestEnabled ? 1 : 0)}/{(depth.State.DepthWriteEnabled ? 1 : 0)}/{(int)depth.State.DepthCompare} " + + $"viewportTransform=0x{banks.Context.ScreenViewport.TransformControl:X8} directXClip={clip.DirectXClipSpace} " + + $"clipDisabled={clip.ClipDisable} nearClipDisabled={clip.NearZClipDisable} farClipDisabled={clip.FarZClipDisable} " + + $"zScale={viewport.ZScale:R} zOffset={viewport.ZOffset:R} zBounds={viewport.MinDepth:R}/{viewport.MaxDepth:R} " + + $"hostDepthRange={viewport.ZOffset - (clip.DirectXClipSpace ? 0f : viewport.ZScale):R}/{viewport.ZScale + viewport.ZOffset:R}"); } } diff --git a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs index 131bf95..e363383 100644 --- a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs @@ -3,6 +3,7 @@ using SharpEmu.HLE; using SharpEmu.Libs.Ampr; +using System.Buffers.Binary; using System.Collections.Concurrent; using System.Threading; @@ -16,7 +17,12 @@ public static class KernelAprCompatExports private static readonly bool _traceApr = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal); - private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress); + private readonly record struct AprSubmission( + ulong CommandBuffer, + ulong Priority, + ulong ResultAddress, + int ExecutionResult, + uint ErrorOffset); [SysAbiExport( Nid = "ASoW5WE-UPo", @@ -41,20 +47,24 @@ public static class KernelAprCompatExports submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); } - _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress); - - var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); + var completionResult = AmprExports.CompleteCommandBuffer( + ctx, + commandBuffer, + out var executionResult, + out var errorOffset); if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) { return completionResult; } + _submittedCommandBuffers[submissionId] = + new AprSubmission(commandBuffer, priority, resultAddress, executionResult, errorOffset); if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress)) + if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress, executionResult, errorOffset)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -80,7 +90,12 @@ public static class KernelAprCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - // Completion output was written when the command was submitted. + if (submission.ResultAddress != 0 && + !TryWriteAprResult(ctx, submission.ResultAddress, submission.ExecutionResult, submission.ErrorOffset)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -99,13 +114,17 @@ public static class KernelAprCompatExports } var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); - _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0); - - var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); + var completionResult = AmprExports.CompleteCommandBuffer( + ctx, + commandBuffer, + out var executionResult, + out var errorOffset); if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) { return completionResult; } + _submittedCommandBuffers[submissionId] = + new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], 0, executionResult, errorOffset); TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -126,13 +145,17 @@ public static class KernelAprCompatExports } var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); - _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0); - - var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); + var completionResult = AmprExports.CompleteCommandBuffer( + ctx, + commandBuffer, + out var executionResult, + out var errorOffset); if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) { return completionResult; } + _submittedCommandBuffers[submissionId] = + new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], 0, executionResult, errorOffset); if (!ctx.TryWriteUInt32(outSubmissionId, submissionId)) { @@ -156,10 +179,15 @@ public static class KernelAprCompatExports return ctx.SetReturn(0); } - private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress) + private static bool TryWriteAprResult( + CpuContext ctx, + ulong resultAddress, + int executionResult, + uint errorOffset) { Span result = stackalloc byte[sizeof(ulong)]; - result.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(result, executionResult); + BinaryPrimitives.WriteUInt32LittleEndian(result[sizeof(int)..], errorOffset); return ctx.Memory.TryWrite(resultAddress, result); } diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index 35953eb..190ebff 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -903,6 +903,17 @@ public static class KernelEventQueueCompatExports return true; } + internal static bool TriggerAmprEvent(ulong handle, ulong ident, ulong data) + { + return TriggerRegisteredEvent( + handle, + ident, + KernelEventFilterAmpr, + 0, + data, + preserveRegisteredUserData: true); + } + public static bool RegisterEvent( ulong handle, ulong ident, @@ -1279,7 +1290,9 @@ public static class KernelEventQueueCompatExports ulong handle, ulong ident, short filter, - ulong userData) + ulong userData, + ulong data = 0, + bool preserveRegisteredUserData = false) { EventQueueState state; lock (_eventQueueGate) @@ -1305,8 +1318,8 @@ public static class KernelEventQueueCompatExports registration.Filter, registration.Flags, 0, - 0, - userData)); + data, + preserveRegisteredUserData ? registration.UserData : userData)); } WakeEventQueue( diff --git a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs index 15abf57..1b92cfa 100644 --- a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs +++ b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs @@ -367,8 +367,9 @@ internal static class RenderPhaseProfile } // Cache calls on other threads must not enter the render-thread counters. + // Check the switch first to avoid thread-local storage allocation when profiling is disabled. internal static Scope MeasureDetail(Phase phase) => - _scopeDepth > 0 ? Measure(phase) : default; + Enabled && _scopeDepth > 0 ? Measure(phase) : default; internal static bool DetailMeasurementsEnabled => Enabled && _scopeDepth > 0; diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 0b47391..0470bbb 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -72,6 +72,7 @@ public static partial class VideoOutExports // sceVideoOutGetEventId (mapped below), so the exact value is internal; only // its distinctness from the flip ident matters for GetEventId/GetEventData. private const ulong SceVideoOutInternalEventVblank = 0x40; + private const ulong SceVideoOutInternalEventOutputMode = 0x8; private const short OrbisKernelEventFilterVideoOut = -13; private static readonly object _stateGate = new(); @@ -239,6 +240,7 @@ public static partial class VideoOutExports public uint OutputWidth { get; set; } = 1920; public uint OutputHeight { get; set; } = 1080; public uint RefreshRate { get; set; } = 60; + public ulong OutputMode { get; set; } = SceVideoOutOutputModeDefault; public float Gamma { get; set; } = 1.0f; public Dictionary LatencyStartPoints { get; } = new(); public Queue<(long FlipArg, long Timestamp)> LatencyStartPointOrder { get; } = new(); @@ -249,6 +251,7 @@ public static partial class VideoOutExports public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots(); public List FlipEvents { get; } = new(); public List VblankEvents { get; } = new(); + public List OutputModeEvents { get; } = new(); public long OpenTimestamp; public long LastCpuFlipTimestamp = -1; public int WindowTop; @@ -435,10 +438,31 @@ public static partial class VideoOutExports if (supported < 0) return supported; if (supported == 0) return OrbisVideoOutErrorUnavailableOutputMode; var handle = unchecked((int)ctx[CpuRegister.Rdi]); - // The only advertised configuration is the current default output. - return TryGetPort(handle, out _) - ? (int)OrbisGen2Result.ORBIS_GEN2_OK - : OrbisVideoOutErrorInvalidHandle; + var mode = ctx[CpuRegister.Rsi]; + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + FlipEventRegistration[] registrations; + lock (_stateGate) + { + port.OutputMode = mode; + registrations = port.OutputModeEvents.ToArray(); + } + + var eventHint = (mode & 0x0000_FFFF_FFFF_FFFFUL) << 16; + foreach (var registration in registrations) + { + _ = KernelEventQueueCompatExports.TriggerDisplayEvent( + registration.Equeue, + SceVideoOutInternalEventOutputMode, + OrbisKernelEventFilterVideoOut, + eventHint, + registration.UserData); + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( @@ -884,6 +908,52 @@ public static partial class VideoOutExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "kmSe30JTs+E", + ExportName = "sceVideoOutAddOutputModeEvent", + Target = Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutAddOutputModeEvent(CpuContext ctx) + { + var equeue = ctx[CpuRegister.Rdi]; + var handle = unchecked((int)ctx[CpuRegister.Rsi]); + var userData = ctx[CpuRegister.Rdx]; + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + if (!KernelEventQueueCompatExports.IsValidEqueue(equeue)) + { + return OrbisVideoOutErrorInvalidEventQueue; + } + + ulong outputMode; + lock (_stateGate) + { + var existingIndex = port.OutputModeEvents.FindIndex(registration => registration.Equeue == equeue); + if (existingIndex >= 0) + { + port.OutputModeEvents[existingIndex] = new FlipEventRegistration(equeue, userData); + } + else + { + port.OutputModeEvents.Add(new FlipEventRegistration(equeue, userData)); + } + outputMode = port.OutputMode; + } + + var eventHint = (outputMode & 0x0000_FFFF_FFFF_FFFFUL) << 16; + return KernelEventQueueCompatExports.TriggerDisplayEvent( + equeue, + SceVideoOutInternalEventOutputMode, + OrbisKernelEventFilterVideoOut, + eventHint, + userData) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : OrbisVideoOutErrorInvalidEventQueue; + } + [SysAbiExport( Nid = "oNOQn3knW6s", ExportName = "sceVideoOutDeleteVblankEvent", @@ -1025,6 +1095,11 @@ public static partial class VideoOutExports return 1; } + if (ident == SceVideoOutInternalEventOutputMode) + { + return 8; + } + return OrbisVideoOutErrorInvalidEvent; } @@ -1050,7 +1125,9 @@ public static partial class VideoOutExports } if (filter != OrbisKernelEventFilterVideoOut || - (ident != SceVideoOutInternalEventFlip && ident != SceVideoOutInternalEventVblank)) + (ident != SceVideoOutInternalEventFlip && + ident != SceVideoOutInternalEventVblank && + ident != SceVideoOutInternalEventOutputMode)) { return OrbisVideoOutErrorInvalidEvent; } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 6bd8452..93255c0 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -455,13 +455,13 @@ internal static unsafe partial class VulkanVideoPresenter } } - public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) + public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) { using var translationScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.CommandDispatchTranslation); var started = System.Diagnostics.Stopwatch.GetTimestamp(); try { - _translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator); + _translation.Dispatch(submitId, groupsX, groupsY, groupsZ, dispatchInitiator, indirectArgumentsAddress); } finally { diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs index ecffa6b..b156666 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs @@ -87,9 +87,14 @@ internal static unsafe partial class VulkanVideoPresenter private static ShaderImageShape ShapeOf(ImageResource image) => new( Volume: image.Dimension == ImageDimension.Dim3D, Arrayed: image.Cube || image.Dimension is ImageDimension.Dim1DArray or ImageDimension.Dim2DArray or ImageDimension.Dim2DMsaaArray, + Cube: image.Cube, Storage: image.ResourceClass == ShaderCompiler.Resources.ImageResourceClass.Storage, DynamicMip: image.MipMode == ImageMipMode.DynamicStorage, - NumericClass: NumericClassOf(image)); + NumericClass: NumericClassOf(image), + OneDimensional: image.Dimension is ImageDimension.Dim1D or ImageDimension.Dim1DArray, + R128: image.R128, + Multisampled: image.Dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray, + DepthCompare: image.DepthCompare); // Render-state discovery for one shader image; the view is acquired later with the draw. private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index e539293..7516eb7 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -628,6 +628,9 @@ internal static unsafe partial class VulkanVideoPresenter $"mode={graphicsSubgroupMode} compute_subgroups=unchanged"); } + private bool _supportsFragmentShaderBarycentric; + private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric"; + private void CreateDevice() { var priority = 1.0f; @@ -708,6 +711,21 @@ internal static unsafe partial class VulkanVideoPresenter "guest BC1-BC7 textures cannot be sampled directly."); } + var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR + { + SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr, + }; + if (IsDeviceExtensionAvailable(FragmentShaderBarycentricExtensionName)) + { + var barycentricQuery = new PhysicalDeviceFeatures2 + { + SType = StructureType.PhysicalDeviceFeatures2, + PNext = &barycentricFeatures, + }; + _vk.GetPhysicalDeviceFeatures2(_physicalDevice, &barycentricQuery); + _supportsFragmentShaderBarycentric = barycentricFeatures.FragmentShaderBarycentric; + } + var depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT { SType = StructureType.PhysicalDeviceDepthClipEnableFeaturesExt, @@ -809,15 +827,20 @@ internal static unsafe partial class VulkanVideoPresenter var colorWriteEnableExtension = (byte*)SilkMarshal.StringToPtr(ColorWriteEnableExtensionName); var depthClipControlExtension = (byte*)SilkMarshal.StringToPtr(DepthClipControlExtensionName); var depthClipEnableExtension = (byte*)SilkMarshal.StringToPtr(DepthClipEnableExtensionName); + var barycentricExtension = (byte*)SilkMarshal.StringToPtr(FragmentShaderBarycentricExtensionName); try { - var extensions = stackalloc byte*[11]; + var extensions = stackalloc byte*[12]; var extensionCount = 0u; extensions[extensionCount++] = swapchainExtension; extensions[extensionCount++] = pushDescriptorExtension; extensions[extensionCount++] = dynamicRenderingExtension; extensions[extensionCount++] = extendedDynamicStateExtension; extensions[extensionCount++] = extendedDynamicState2Extension; + if (_supportsFragmentShaderBarycentric) + { + extensions[extensionCount++] = barycentricExtension; + } if (supportsColorWriteEnable) { extensions[extensionCount++] = colorWriteEnableExtension; @@ -880,6 +903,11 @@ internal static unsafe partial class VulkanVideoPresenter PNext = &timelineSemaphoreFeatures, }; void* renderingChain = &addressFeatures; + if (_supportsFragmentShaderBarycentric) + { + barycentricFeatures.PNext = renderingChain; + renderingChain = &barycentricFeatures; + } if (_supportsDepthClipEnable) { depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT @@ -962,6 +990,7 @@ internal static unsafe partial class VulkanVideoPresenter SilkMarshal.Free((nint)depthClipControlExtension); SilkMarshal.Free((nint)depthClipEnableExtension); SilkMarshal.Free((nint)pushDescriptorExtension); + SilkMarshal.Free((nint)barycentricExtension); } _vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs index 9e2c89b..b522a5f 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs @@ -11,6 +11,7 @@ using SharpEmu.Libs.Gpu.Rendering; using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; using Silk.NET.Core.Native; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; @@ -228,6 +229,12 @@ internal static unsafe partial class VulkanVideoPresenter private ShaderModule CreateShaderModule(byte[] code) { + if (!_supportsFragmentShaderBarycentric && RequiresFragmentShaderBarycentric(code)) + { + throw new NotSupportedException( + "The shader requires the fragmentShaderBarycentric device feature."); + } + string? dumpPath = null; var dumpDirectory = Environment.GetEnvironmentVariable("SHARPEMU_SHADER_SPIRV_DUMP_DIR"); if (!string.IsNullOrWhiteSpace(dumpDirectory)) @@ -263,6 +270,28 @@ internal static unsafe partial class VulkanVideoPresenter } } + private static bool RequiresFragmentShaderBarycentric(ReadOnlySpan code) + { + for (var offset = 20; offset + 4 <= code.Length;) + { + var header = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[offset..]); + var wordCount = (int)(header >> 16); + if (wordCount == 0 || wordCount > (code.Length - offset) / 4) + { + throw new InvalidOperationException("The shader contains an invalid SPIR-V instruction."); + } + + if ((header & 0xFFFF) == (uint)SpirvOp.Capability && wordCount == 2 && + System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[(offset + 4)..]) == + (uint)SpirvCapability.FragmentBarycentricKhr) + { + return true; + } + offset += wordCount * 4; + } + return false; + } + // One layout binding per descriptor binding of the stage, at the stage's native binding numbers. private static void CollectLayoutBindings(List bindings, ShaderProgramInfo program, ShaderStage stage) { @@ -413,7 +442,7 @@ internal static unsafe partial class VulkanVideoPresenter throw SubmissionScheduler.Fatal($"A vertex attribute buffer is swizzled: attribute={index} hash=0x{description.VertexStage.Hash:X16}."); } - CheckVertexSwizzle(in descriptor, resource.RegisterCount, attributeSize, index); + CheckVertexSwizzle(in descriptor, (int)usedComponents, attributeSize, index); attributes[index] = new VertexInputAttributeDescription { Location = (uint)index, @@ -427,12 +456,12 @@ internal static unsafe partial class VulkanVideoPresenter private static uint DestinationSelect(uint x, uint y = 0, uint z = 0, uint w = 0) => x | (y << 3) | (z << 6) | (w << 9); // A destination select the fixed-function fetch cannot apply is logged once and accepted. - private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int registerCount, uint attributeSize, int index) + private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int fetchedComponents, uint attributeSize, int index) { uint swizzle; uint expected; var supported = true; - switch (registerCount) + switch (fetchedComponents) { case 1: swizzle = descriptor.DestinationSelectX; @@ -478,14 +507,14 @@ internal static unsafe partial class VulkanVideoPresenter break; default: - throw SubmissionScheduler.Fatal($"A vertex attribute fills an invalid register count: attribute={index} registers={registerCount}."); + throw SubmissionScheduler.Fatal($"A vertex attribute fetch uses an invalid component count: attribute={index} components={fetchedComponents}."); } if (!supported && Interlocked.Exchange(ref _vertexSwizzleLines, 1) == 0) { Console.Error.WriteLine( $"[LOADER][INFO] vertex_input accepted an unsupported destination select attribute={index} size={attributeSize} " + - $"registers={registerCount} swizzle=0x{swizzle:X3} expected=0x{expected:X3}"); + $"components={fetchedComponents} swizzle=0x{swizzle:X3} expected=0x{expected:X3}"); } } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index 9fda397..fd7bab2 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -827,6 +827,28 @@ internal static unsafe partial class VulkanVideoPresenter CountDraw(); } + public bool TryDispatchIndirect(ulong argumentsAddress) + { + using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawRecording); + var (buffer, offset) = _bufferCache.ObtainBuffer(argumentsAddress, 3u * sizeof(uint), false); + var command = BeginBatchedGuestCommands(); + var barrier = new BufferMemoryBarrier + { + SType = StructureType.BufferMemoryBarrier, + SrcAccessMask = AccessFlags.ShaderWriteBit | AccessFlags.TransferWriteBit | AccessFlags.MemoryWriteBit, + DstAccessMask = AccessFlags.IndirectCommandReadBit, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Buffer = buffer.Handle, + Offset = offset, + Size = 3u * sizeof(uint), + }; + _vk.CmdPipelineBarrier(command, PipelineStageFlags.AllCommandsBit, PipelineStageFlags.DrawIndirectBit, 0, 0, null, 1, &barrier, 0, null); + _vk.CmdDispatchIndirect(command, buffer.Handle, offset); + CountDraw(); + return true; + } + private void RecordMemoryBarrier(PipelineStageFlags sourceStages, PipelineStageFlags destinationStages, AccessFlags sourceAccess, AccessFlags destinationAccess) { EndRendering(); @@ -917,6 +939,17 @@ internal static unsafe partial class VulkanVideoPresenter _ = BeginBatchedGuestCommands(); } + public void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) + { + EndRendering(); + _imageCache.RefreshImage(source); + _imageCache.RefreshImage(destination); + var sourceImage = _imageCache.GetImage(source); + var destinationImage = _imageCache.GetImage(destination); + destinationImage.CopyDepthStencilFrom(sourceImage, in range, in extent, aspects); + _ = BeginBatchedGuestCommands(); + } + public bool IsMetadata(ulong address) => _imageCache.IsMetadata(address); public bool ClearMetadata(ulong address) diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslArgumentLayout.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslArgumentLayout.cs index 683ccd7..360b404 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslArgumentLayout.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslArgumentLayout.cs @@ -123,7 +123,8 @@ public sealed class Gen5MslArgumentLayout ImageNumericClass.Sint => "sint", _ => "float", }; - return $"{classPart}_{numericPart}_{DimensionName(dimension)}"; + var dimensionPart = ImageDescriptorBinding.IsCube(kind) ? "cube" : DimensionName(dimension); + return $"{classPart}_{numericPart}_{dimensionPart}"; } public static string DimensionName(ImageDimension dimension) => dimension switch @@ -138,9 +139,9 @@ public sealed class Gen5MslArgumentLayout }; // The Metal texture type of an image class with the given component and access. - public static string TextureType(ImageDimension dimension, string componentType, string? access) + public static string TextureType(ImageDimension dimension, string componentType, string? access, bool cube = false) { - var typeName = dimension switch + var typeName = cube ? "texturecube" : dimension switch { ImageDimension.Dim1D => "texture1d", ImageDimension.Dim1DArray => "texture1d_array", diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs index 32faee2..2a63752 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs @@ -203,6 +203,10 @@ public static partial class Gen5MslTranslator // form is fma(src0, src1, src2) exactly like the SPIR-V translator. "VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" => FloatResult(instruction, $"fma({F(instruction, 0)}, {F(instruction, 1)}, {F(instruction, 2)})"), + "VFmaF16" => Float16Result( + instruction, + destination, + $"fma({F16(instruction, 0)}, {F16(instruction, 1)}, {F16(instruction, 2)})"), "VFmacF32" or "VMacF32" => FloatResult(instruction, $"fma({F(instruction, 0)}, {F(instruction, 1)}, as_type(v[{destination}]))"), "VFloorF32" => FloatResult(instruction, $"floor({F(instruction, 0)})"), @@ -212,6 +216,10 @@ public static partial class Gen5MslTranslator "VFractF32" => FloatResult(instruction, $"fract({F(instruction, 0)})"), "VSqrtF32" => FloatResult(instruction, $"sqrt({F(instruction, 0)})"), "VRsqF32" => FloatResult(instruction, $"rsqrt({F(instruction, 0)})"), + "VRsqF16" => Float16Result( + instruction, + destination, + $"rsqrt({F16(instruction, 0)})"), "VRcpF32" or "VRcpIflagF32" => FloatResult(instruction, $"(1.0f / {F(instruction, 0)})"), "VLogF32" => FloatResult(instruction, $"log2({F(instruction, 0)})"), "VExpF32" => FloatResult(instruction, $"exp2({F(instruction, 0)})"), diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs index 59d12d5..b8af129 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Pixel.cs @@ -650,18 +650,36 @@ public static partial class Gen5MslTranslator for (uint component = 0; component < control.DwordCount; component++) { - if (component >= input.ComponentCount) + var selector = (input.DestinationSelect >> (int)(component * 3)) & 0x7u; + if (selector == 0) { - // Formatted buffer loads return zero for components that - // are not present in the resource format. StoreVector(control.VectorData + component, "0u"); } + else if (selector == 1) + { + StoreVector( + control.VectorData + component, + input.NumberFormat is 4u or 5u ? "1u" : "0x3f800000u"); + } + else if (selector is >= 4u and <= 7u) + { + var sourceComponent = selector - 4u; + if (sourceComponent >= input.ComponentCount) + { + error = + $"vertex input destination selector exceeds source components: selector={selector} components={input.ComponentCount}"; + return false; + } + + var value = input.ComponentCount == 1 + ? $"sharpemu_vin.in{input.Location}" + : $"sharpemu_vin.in{input.Location}[{sourceComponent}]"; + StoreVector(control.VectorData + component, AsUInt(value)); + } else { - var value = input.ComponentCount == 1 - ? $"sharpemu_vin.in{input.Location}" - : $"sharpemu_vin.in{input.Location}[{component}]"; - StoreVector(control.VectorData + component, AsUInt(value)); + error = $"unsupported vertex input destination selector={selector}"; + return false; } } diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Resources.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Resources.cs index b570cb4..a36f811 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Resources.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Resources.cs @@ -147,6 +147,7 @@ public static partial class Gen5MslTranslator private void DeclareImageClass(DescriptorBinding binding, ShaderResourceInfo info) { var (resourceClass, numericClass, dimension, atomic) = ImageDescriptorBinding.Describe(binding.Kind); + var cube = ImageDescriptorBinding.IsCube(binding.Kind); if (resourceClass == ImageResourceClass.None) { throw new InvalidOperationException($"binding kind {binding.Kind} is not an image class"); @@ -177,7 +178,7 @@ public static partial class Gen5MslTranslator var multisampled = dimension is ImageDimension.Dim2DMsaa or ImageDimension.Dim2DMsaaArray; _imageClasses[binding.Kind] = new MslImageClass( Gen5MslArgumentLayout.ImageClassName(binding.Kind), - Gen5MslArgumentLayout.TextureType(dimension, componentKind, access), + Gen5MslArgumentLayout.TextureType(dimension, componentKind, access, cube), componentKind, isStorage, multisampled, @@ -465,7 +466,8 @@ public static partial class Gen5MslTranslator value = Temp("uint", $"sharpemu_load_device_dword({DeviceArguments}, {componentAddress})"); } - if (_indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch)) + if (!request.IndirectOffsetKeyMemoryIndices.Contains(memoryIndex) && + _indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch)) { Line($"{keyScratch} = {value};"); } diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs index 33bdee4..481078b 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs @@ -332,9 +332,15 @@ public static partial class Gen5MslTranslator continue; } + var componentType = input.NumberFormat switch + { + 4u => "uint", + 5u => "int", + _ => "float", + }; var fieldType = input.ComponentCount == 1 - ? "float" - : $"float{input.ComponentCount}"; + ? componentType + : $"{componentType}{input.ComponentCount}"; source.AppendLine( $" {fieldType} in{input.Location} [[attribute({input.Location})]];"); } @@ -1010,6 +1016,7 @@ public static partial class Gen5MslTranslator int bindingIndex; string stride; string descriptorWord3; + uint descriptorFormat; { // The dense buffer, its stride and its format come from the specialization. if (!TryResolveLayoutBuffer(instruction.Pc, out bindingIndex, out var specialized)) @@ -1019,6 +1026,7 @@ public static partial class Gen5MslTranslator } stride = FormatUInt(specialized.PackedStride & 0x3FFF); + descriptorFormat = specialized.DescriptorFormat; descriptorWord3 = FormatUInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF)); } @@ -1036,9 +1044,26 @@ public static partial class Gen5MslTranslator ApplyByteBias( bindingIndex, $"(0x{unchecked((uint)control.OffsetBytes):X}u + {scalarOffset} + {vectorOffset} + ({vectorIndex} * {stride}))")); + if (instruction.Opcode is "BufferStoreFormatX" or "BufferStoreFormatXy" or + "BufferStoreFormatXyz" or "BufferStoreFormatXyzw") + { + if (descriptorFormat == 0) + { + return true; + } + + if (!TryEmitBufferFormatStore(bindingIndex, address, control, descriptorWord3, descriptorFormat)) + { + error = $"unsupported buffer store format {descriptorFormat}"; + return false; + } + + return true; + } + if (control.Typed && instruction.Opcode.StartsWith("TBufferStore", StringComparison.Ordinal) && - TryEmitTypedBufferFormatStore(bindingIndex, address, control, descriptorWord3)) + TryEmitBufferFormatStore(bindingIndex, address, control, descriptorWord3, control.TypedFormat)) { return true; } @@ -1181,15 +1206,16 @@ public static partial class Gen5MslTranslator return true; } - // A typed store converts each register with the format's number format and places + // A formatted store converts each register with the selected number format and places // the bits at the component's offset; all transferred components are stored or dropped. - private bool TryEmitTypedBufferFormatStore( + private bool TryEmitBufferFormatStore( int bindingIndex, string byteAddress, Gen5BufferMemoryControl control, - string descriptorWord3) + string descriptorWord3, + uint unifiedFormat) { - if (!Gfx10UnifiedFormat.TryDecode(control.TypedFormat, out var dataFormat, out var numberFormat)) + if (!Gfx10UnifiedFormat.TryDecode(unifiedFormat, out var dataFormat, out var numberFormat)) { return false; } diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 4b1adf1..0f2ba19 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -115,26 +115,19 @@ public static partial class Gen5SpirvTranslator break; case "VWritelaneB32": { - // vdst[lane(src1)] = src0 - // Per-lane: if current lane == src1, write src0, else keep old value. var oldValue = LoadV(destination); - var src0 = GetRawSource(instruction, 0); - var laneSelect = GetRawSource(instruction, 1); - var currentLane = _subgroupInvocationIdInput != 0 - ? BitwiseAnd( - Load(_uintType, _subgroupInvocationIdInput), - UInt(RdnaWaveLaneCount - 1)) - : UInt(0); + var sourceValue = GetRawSource(instruction, 0); + var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1)); var isTargetLane = _module.AddInstruction( SpirvOp.IEqual, _boolType, - currentLane, - laneSelect); + GuestWaveLane(), + selectedLane); result = _module.AddInstruction( SpirvOp.Select, _uintType, isTargetLane, - src0, + sourceValue, oldValue); // Writelane writes to a specific lane regardless of exec mask. StoreV(destination, result, guardWithExec: false); @@ -283,6 +276,12 @@ public static partial class Gen5SpirvTranslator instruction, Ext(32, _floatType, GetFloatSource(instruction, 0))); break; + case "VRsqF16": + result = EmitFloat16Result( + instruction, + destination, + Ext(32, _floatType, GetFloat16Source(instruction, 0))); + break; case "VFractF32": result = EmitFloatResult( instruction, @@ -392,6 +391,17 @@ public static partial class Gen5SpirvTranslator GetFloatSource(instruction, 1), GetFloatSource(instruction, 2))); break; + case "VFmaF16": + result = EmitFloat16Result( + instruction, + destination, + Ext( + 50, + _floatType, + GetFloat16Source(instruction, 0), + GetFloat16Source(instruction, 1), + GetFloat16Source(instruction, 2))); + break; case "VMacF32": case "VFmacF32": { @@ -455,6 +465,12 @@ public static partial class Gen5SpirvTranslator case "VAddU32": result = EmitIntegerBinary(instruction, SpirvOp.IAdd); break; + case "VAddNcU16": + result = EmitInteger16Binary( + instruction, + destination, + SpirvOp.IAdd); + break; case "VAddcU32": case "VAddCoCiU32": result = EmitAddWithCarry(instruction); @@ -1189,18 +1205,6 @@ public static partial class Gen5SpirvTranslator return false; } - var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2; - for (var index = 0; index < sourceCount; index++) - { - var source = instruction.Sources[index]; - if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister)) - { - error = - $"unsupported vop3p operand {source} for {instruction.Opcode} (first slice: registers only)"; - return false; - } - } - var low = EmitPackedF16Lane(instruction, control, highLane: false); var high = EmitPackedF16Lane(instruction, control, highLane: true); result = BitwiseOr(low, ShiftLeftLogical(high, UInt(16))); @@ -1779,8 +1783,14 @@ public static partial class Gen5SpirvTranslator } else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32")) { - var left = GetRawSource(instruction, 0); - var right = GetRawSource(instruction, 1); + var compare64 = opcode.EndsWith("U64", StringComparison.Ordinal) || + opcode.EndsWith("I64", StringComparison.Ordinal); + var left = compare64 + ? GetRawSource64(instruction, 0) + : GetRawSource(instruction, 0); + var right = compare64 + ? GetRawSource64(instruction, 1) + : GetRawSource(instruction, 1); var signed = opcode.EndsWith("I32", StringComparison.Ordinal); if (signed) { @@ -1794,6 +1804,7 @@ public static partial class Gen5SpirvTranslator "VCmpEqU32" or "VCmpxEqU32" => SpirvOp.IEqual, "VCmpNeI32" or "VCmpxNeI32" or "VCmpNeU32" or "VCmpxNeU32" => SpirvOp.INotEqual, + "VCmpxNeU64" => SpirvOp.INotEqual, "VCmpLtI32" or "VCmpxLtI32" => SpirvOp.SLessThan, "VCmpLeI32" or "VCmpxLeI32" => SpirvOp.SLessThanEqual, "VCmpGtI32" or "VCmpxGtI32" => SpirvOp.SGreaterThan, @@ -3595,6 +3606,41 @@ public static partial class Gen5SpirvTranslator return _module.AddInstruction(operation, _uintType, left, right); } + private uint EmitInteger16Binary( + Gen5ShaderInstruction instruction, + uint destination, + SpirvOp operation) + { + var control = instruction.Control as Gen5Vop3Control; + var left = GetRawSource(instruction, 0, applySdwaIntegerModifiers: false); + var right = GetRawSource(instruction, 1, applySdwaIntegerModifiers: false); + + if ((control?.OperandSelect & 0x1) != 0) + { + left = ShiftRightLogical(left, UInt(16)); + } + + if ((control?.OperandSelect & 0x2) != 0) + { + right = ShiftRightLogical(right, UInt(16)); + } + + left = BitwiseAnd(left, UInt(0xFFFF)); + right = BitwiseAnd(right, UInt(0xFFFF)); + var low16 = BitwiseAnd( + _module.AddInstruction(operation, _uintType, left, right), + UInt(0xFFFF)); + var current = LoadV(destination); + + return (control?.OperandSelect & 0x8) != 0 + ? BitwiseOr( + BitwiseAnd(current, UInt(0x0000_FFFF)), + ShiftLeftLogical(low16, UInt(16))) + : BitwiseOr( + BitwiseAnd(current, UInt(0xFFFF_0000)), + low16); + } + private enum CubeCoordinate { Id, @@ -3946,24 +3992,35 @@ public static partial class Gen5SpirvTranslator } var destination = instruction.Destinations[0].Value; - var src0 = GetRawSource(instruction, 0); + var sourceValue = GetRawSource(instruction, 0); + var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1)); - if (_subgroupInvocationIdInput != 0) + if (_emulateWave64) + { + // The selected guest lane can belong to another host subgroup. + // Read it even when the guest execution mask disables that lane. + var isSelectedLane = _module.AddInstruction( + SpirvOp.IEqual, _boolType, GuestWaveLane(), selectedLane); + EmitConditional(isSelectedLane, () => Store(WaveBroadcastScratchPointer(), sourceValue)); + EmitWave64Barrier(); + var broadcast = Load(_uintType, WaveBroadcastScratchPointer()); + EmitWave64Barrier(); + StoreS(destination, broadcast); + } + else if (_subgroupInvocationIdInput != 0) { - // sdst = vsrc0[lane(src1)] — broadcast from the specified lane. - var laneSelect = GetRawSource(instruction, 1); var broadcast = _module.AddInstruction( SpirvOp.GroupNonUniformBroadcast, _uintType, - UInt(3), // Subgroup scope - src0, - laneSelect); + UInt(3), + sourceValue, + selectedLane); StoreS(destination, broadcast); } else { // Fallback: no subgroup ops, read current lane's value. - StoreS(destination, src0); + StoreS(destination, sourceValue); } return true; diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs new file mode 100644 index 0000000..d297c93 --- /dev/null +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Interpolation.cs @@ -0,0 +1,141 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.ShaderCompiler; + +namespace SharpEmu.ShaderCompiler.Vulkan; + +public static partial class Gen5SpirvTranslator +{ + private sealed partial class CompilationContext + { + private readonly HashSet _perVertexAttributes = []; + private readonly Dictionary _barycentricInputs = []; + private uint _interpolationSampleId; + private const uint InterpolateAtCentroid = 76; + private const uint InterpolateAtSample = 77; + private const uint InterpolateAtOffset = 78; + + private void DeclareInterpolationParameters() + { + foreach (var instruction in _request.Program.Instructions) + { + if (instruction.Opcode == "VInterpMovF32" && + instruction.Control is Gen5InterpolationControl interpolation) + { + _perVertexAttributes.Add(interpolation.Attribute); + } + } + + if (_perVertexAttributes.Count == 0) + { + return; + } + + _module.AddCapability(SpirvCapability.FragmentBarycentricKhr); + _module.AddExtension("SPV_KHR_fragment_shader_barycentric"); + _module.AddCapability(SpirvCapability.InterpolationFunction); + var enabledInputs = _pixelInputAddress & _pixelInputEnable; + if ((enabledInputs & (1u << 3)) != 0) + { + throw new NotSupportedException("Pull-model interpolation parameters are not supported."); + } + + var variables = new Dictionary(); + foreach (var bit in new[] { 0, 1, 2, 4, 5, 6 }) + { + if ((enabledInputs & (1u << bit)) == 0) + { + continue; + } + + if (!variables.TryGetValue(bit < 4, out var variable)) + { + variable = _module.AddGlobalVariable( + _module.TypePointer(SpirvStorageClass.Input, _vec3Type), SpirvStorageClass.Input); + _module.AddDecoration(variable, SpirvDecoration.BuiltIn, + (uint)(bit < 4 ? SpirvBuiltIn.BaryCoordKhr : SpirvBuiltIn.BaryCoordNoPerspKhr)); + variables.Add(bit < 4, variable); + _interfaces.Add(variable); + } + _barycentricInputs.Add(bit, variable); + } + + if ((enabledInputs & 0x11u) != 0) + { + _module.AddCapability(SpirvCapability.SampleRateShading); + _interpolationSampleId = _module.AddGlobalVariable( + _module.TypePointer(SpirvStorageClass.Input, _intType), SpirvStorageClass.Input); + _module.AddDecoration(_interpolationSampleId, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.SampleId); + _module.AddDecoration(_interpolationSampleId, SpirvDecoration.Flat); + _interfaces.Add(_interpolationSampleId); + } + } + + private uint LoadBarycentricCoordinates(int bit, uint variable) => bit switch + { + 0 or 4 => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtSample, + variable, Load(_intType, _interpolationSampleId)), + 2 or 6 => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtCentroid, variable), + _ => _module.AddInstruction(SpirvOp.ExtInst, _vec3Type, _glsl, InterpolateAtOffset, + variable, _module.ConstantNull(_vec2Type)), + }; + + private bool TryEmitInterpolationParameter( + Gen5ShaderInstruction instruction, + Gen5InterpolationControl interpolation, + uint input, + uint destination, + out string error) + { + error = string.Empty; + var custom = interpolation.Attribute < 32 && + (_request.PixelCustomInterpolationMask & (1u << (int)interpolation.Attribute)) != 0; + + uint LoadVertex(uint vertex) + { + var pointer = _module.AddInstruction(SpirvOp.AccessChain, + _module.TypePointer(SpirvStorageClass.Input, _floatType), + input, UInt(vertex), UInt(interpolation.Channel)); + return Load(_floatType, pointer); + } + + uint LoadParameter(uint mode) + { + var value = LoadVertex((mode + 1) % 3); + return !custom && mode < 2 + ? _module.AddInstruction(SpirvOp.FSub, _floatType, value, LoadVertex(0)) + : value; + } + + uint result; + if (instruction.Opcode == "VInterpMovF32") + { + var mode = instruction.Words[0] & 0xFFu; + if (mode >= 3) + { + error = "reserved interpolation parameter selector"; + return false; + } + result = LoadParameter(mode); + } + else if (instruction.Opcode is "VInterpP1F32" or "VInterpP2F32") + { + var firstPhase = instruction.Opcode == "VInterpP1F32"; + var source = Bitcast(_floatType, GetRawSource(instruction, 0)); + var product = _module.AddInstruction(SpirvOp.FMul, _floatType, + LoadParameter(firstPhase ? 0u : 1u), source); + result = _module.AddInstruction(SpirvOp.FAdd, _floatType, product, + firstPhase ? LoadParameter(2) : Bitcast(_floatType, LoadV(destination))); + } + else + { + error = $"unsupported interpolation opcode {instruction.Opcode}"; + return false; + } + + StoreV(destination, Bitcast(_uintType, result)); + return true; + } + } +} diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs index 3fadfe7..52d0d8b 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs @@ -91,6 +91,7 @@ public static partial class Gen5SpirvTranslator ImageComponentKind Kind, bool IsStorage, bool Arrayed, + bool Cube, bool Multisampled, SpirvImageDim Dimension, IReadOnlyList Resources); @@ -307,6 +308,7 @@ public static partial class Gen5SpirvTranslator private void DeclareImageClass(DescriptorBinding binding, uint bindingNumber) { var (resourceClass, numericClass, dimension, atomic) = ImageDescriptorBinding.Describe(binding.Kind); + var cube = ImageDescriptorBinding.IsCube(binding.Kind); if (resourceClass == ImageResourceClass.None) { throw new InvalidOperationException($"binding kind {binding.Kind} is not an image class"); @@ -353,7 +355,7 @@ public static partial class Gen5SpirvTranslator _module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0); _module.AddDecoration(variable, SpirvDecoration.Binding, bindingNumber); _interfaces.Add(variable); - _imageClasses[binding.Kind] = new LayoutImageClass(variable, imageType, elementPointer, componentType, kind, isStorage, arrayed, multisampled, spirvDimension, binding.Resources); + _imageClasses[binding.Kind] = new LayoutImageClass(variable, imageType, elementPointer, componentType, kind, isStorage, arrayed, cube, multisampled, spirvDimension, binding.Resources); } // ---- initial state ---- @@ -721,7 +723,8 @@ public static partial class Gen5SpirvTranslator value = LoadDeviceDword(component == 0 ? deviceAddress.Value : IAdd64(deviceAddress.Value, ULong((ulong)component * sizeof(uint)))); } - if (_indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch)) + if (!request.IndirectOffsetKeyMemoryIndices.Contains(memoryIndex) && + _indirectKeyScratch.TryGetValue(memoryIndex, out var keyScratch)) { Store(keyScratch, value); } @@ -921,6 +924,11 @@ public static partial class Gen5SpirvTranslator var candidateElements = new List<(uint Resource, uint Element)>(); foreach (var candidate in candidates) { + if (candidate >= info.Images.Count) + { + error = $"indirect candidate {candidate} is outside the image table"; + return false; + } var candidateKind = ImageDescriptorBinding.ForImage(info.Images[(int)candidate]); if (candidateKind is null || !_imageClasses.TryGetValue(candidateKind.Value, out var candidateClass)) { @@ -978,6 +986,11 @@ public static partial class Gen5SpirvTranslator } var resourceIndex = (int)(fixedElement?.Resource ?? entry.Resource); + if ((uint)resourceIndex >= info.Images.Count) + { + error = $"image {resourceIndex} is outside the image table"; + return false; + } var imageInfo = info.Images[resourceIndex]; var kind = ImageDescriptorBinding.ForImage(imageInfo); if (kind is null || !_imageClasses.TryGetValue(kind.Value, out var imageClass)) @@ -986,12 +999,6 @@ public static partial class Gen5SpirvTranslator return false; } - if (imageClass.Multisampled) - { - error = "multisampled image access is not supported"; - return false; - } - var element = imageClass.Resources.ToList().IndexOf((uint)resourceIndex); if (element < 0) { @@ -1052,7 +1059,11 @@ public static partial class Gen5SpirvTranslator imageClass.Kind, imageClass.IsStorage, imageClass.Arrayed, - imageClass.Dimension); + imageClass.Cube, + imageClass.Multisampled, + imageClass.Dimension, + imageInfo.ConversionFormat, + imageInfo.ShaderSwizzle); dstSelect = imageInfo.ShaderSwizzle; return true; } @@ -1147,6 +1158,24 @@ public static partial class Gen5SpirvTranslator case "DsReadB32": StoreV(instruction.Destinations[0].Value, LoadBlockWord(_globalDataShare, GlobalDataShareIndex(GetRawSource(instruction, 0), control.SingleOffsetBytes))); return true; + case "DsReadI8": + { + var address = GetRawSource(instruction, 0); + var byteAddress = control.SingleOffsetBytes == 0 + ? address + : IAdd(address, UInt(control.SingleOffsetBytes)); + var word = LoadBlockWord(_globalDataShare, GlobalDataShareIndex(address, control.SingleOffsetBytes)); + var shift = ShiftLeftLogical(BitwiseAnd(byteAddress, UInt(3)), UInt(3)); + var packed = ShiftRightLogical(word, shift); + var signedByte = _module.AddInstruction( + SpirvOp.BitFieldSExtract, + _intType, + Bitcast(_intType, packed), + UInt(0), + UInt(8)); + StoreV(instruction.Destinations[0].Value, Bitcast(_uintType, signedByte)); + return true; + } case "DsReadB64": { var index = GlobalDataShareIndex(GetRawSource(instruction, 0), control.SingleOffsetBytes); @@ -1240,6 +1269,29 @@ public static partial class Gen5SpirvTranslator private bool TryEmitGlobalDataShareAtomic(Gen5ShaderInstruction instruction, Gen5DataShareControl control, out string error) { error = string.Empty; + if (instruction.Opcode is "DsMinF32" or "DsMaxF32") + { + if (instruction.Sources.Count < 3) + { + error = $"missing GDS operands for {instruction.Opcode}"; + return false; + } + + var floatIndex = GlobalDataShareIndex(GetRawSource(instruction, 0), control.SingleOffsetBytes); + EmitExecConditional(() => + { + EmitConditional(IsBlockWordInRange(_globalDataShare, floatIndex), () => + EmitDataShareFloatAtomic( + BlockWordPointer(_globalDataShare, floatIndex), + GetRawSource(instruction, 1), + GetRawSource(instruction, 2), + instruction.Opcode == "DsMaxF32", + scope: 1, + semantics: 0x48)); + }); + return true; + } + var atomicOp = instruction.Opcode switch { "DsAddU32" or "DsAddRtnU32" => SpirvOp.AtomicIAdd, diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 5fc956c..1529b9b 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; namespace SharpEmu.ShaderCompiler.Vulkan; @@ -123,6 +124,13 @@ public static partial class Gen5SpirvTranslator private uint _ldsElementPointer; private uint _ldsDwordMask; private uint _positionOutput; + private uint _pointSizeOutput; + private uint _clipDistanceOutput; + private uint _cullDistanceOutput; + private uint _layerOutput; + private uint _viewportIndexOutput; + private uint _clipDistanceCount; + private uint _cullDistanceCount; private uint _vertexIndexInput; private uint _instanceIndexInput; private uint _fragCoordInput; @@ -161,14 +169,20 @@ public static partial class Gen5SpirvTranslator ImageComponentKind ComponentKind, bool IsStorage, bool Arrayed, - SpirvImageDim Dimension); + bool Cube, + bool Multisampled, + SpirvImageDim Dimension, + uint ConversionFormat, + uint ShaderSwizzle); private readonly record struct SpirvVertexInput( uint Variable, uint Type, uint ComponentType, uint ComponentCount, - VertexInputComponentKind ComponentKind); + VertexInputComponentKind ComponentKind, + uint NumberFormat, + uint DestinationSelect); private readonly record struct SpirvPixelOutput( uint Variable, @@ -780,6 +794,8 @@ public static partial class Gen5SpirvTranslator (uint)SpirvBuiltIn.Position); _interfaces.Add(_positionOutput); + DeclareAuxPositionOutputs(); + var parameters = _request.Program.Instructions .Select(instruction => instruction.Control) .OfType() @@ -819,11 +835,14 @@ public static partial class Gen5SpirvTranslator var locations = Gen5PixelInputMapping.ResolveLocations( _pixelInputCntl, attributes); + DeclareInterpolationParameters(); for (var index = 0; index < attributes.Length; index++) { var attribute = attributes[index]; var variable = _module.AddGlobalVariable( - inputVec4Pointer, + _perVertexAttributes.Contains(attribute) + ? _module.TypePointer(SpirvStorageClass.Input, _module.TypeArray(_vec4Type, 3)) + : inputVec4Pointer, SpirvStorageClass.Input); // VINTRP ATTR selects the PS input slot. SPI_PS_INPUT_CNTL // maps that slot to a VS parameter export location. @@ -834,7 +853,11 @@ public static partial class Gen5SpirvTranslator variable, SpirvDecoration.Location, locations[index]); - if ((cntl & 0x400u) != 0) + if (_perVertexAttributes.Contains(attribute)) + { + _module.AddDecoration(variable, SpirvDecoration.PerVertexKhr); + } + else if ((cntl & 0x400u) != 0) { _module.AddDecoration(variable, SpirvDecoration.Flat); } @@ -946,7 +969,9 @@ public static partial class Gen5SpirvTranslator type, componentType, input.ComponentCount, - componentKind); + componentKind, + input.NumberFormat, + input.DestinationSelect); _vertexInputsByPc.TryAdd(input.Pc, vertexInput); foreach (var aliasPc in input.AliasPcs ?? []) { @@ -1002,6 +1027,20 @@ public static partial class Gen5SpirvTranslator { Store(output, _module.ConstantNull(_vec4Type)); } + if (_pointSizeOutput != 0) + { + Store(_pointSizeOutput, Float(0f)); + } + if (_layerOutput != 0) + { + Store(_layerOutput, UInt(0)); + } + if (_viewportIndexOutput != 0) + { + Store(_viewportIndexOutput, UInt(0)); + } + InitializeDistanceOutput(_clipDistanceOutput, _clipDistanceCount); + InitializeDistanceOutput(_cullDistanceOutput, _cullDistanceCount); } else if (_stage == Gen5SpirvStage.Pixel) { @@ -1088,10 +1127,8 @@ public static partial class Gen5SpirvTranslator { uint vgpr = 0; - // Pixel input VGPRs are compacted in SPI_PS_INPUT_ADDR order. The - // interpolation inputs occupy register slots even though V_INTERP - // is lowered directly from SPIR-V interpolants; position inputs - // following them must still land in the hardware-selected VGPRs. + // Keep input registers in SPI_PS_INPUT_ADDR order, including slots + // whose values are read through the interpolated input interface. AdvancePixelInput(0, 2, ref vgpr); // PERSP_SAMPLE AdvancePixelInput(1, 2, ref vgpr); // PERSP_CENTER AdvancePixelInput(2, 2, ref vgpr); // PERSP_CENTROID @@ -1119,6 +1156,16 @@ public static partial class Gen5SpirvTranslator { if ((_pixelInputAddress & (1u << bit)) != 0) { + if (_barycentricInputs.TryGetValue(bit, out var barycentricInput)) + { + var coordinates = LoadBarycentricCoordinates(bit, barycentricInput); + for (uint component = 0; component < 2; component++) + { + var coordinate = _module.AddInstruction( + SpirvOp.CompositeExtract, _floatType, coordinates, component + 1); + StoreV(vgpr + component, Bitcast(_uintType, coordinate), guardWithExec: false); + } + } vgpr += dwordCount; } } @@ -1358,8 +1405,7 @@ public static partial class Gen5SpirvTranslator // NGG shaders bracket their exports with s_sendmsg // (GS_ALLOC_REQ/DEALLOC) to reserve hardware export space; // exports are translated directly, so the message is moot. - "SSendmsg" or - "VInterpMovF32") + "SSendmsg") { return true; } @@ -1564,6 +1610,30 @@ public static partial class Gen5SpirvTranslator StoreV(instruction.Destinations[0].Value, value); return true; } + case "DsReadI8": + { + if (instruction.Destinations.Count < 1 || instruction.Sources.Count < 1) + { + error = "missing LDS signed byte read operand"; + return false; + } + + var address = GetRawSource(instruction, 0); + var byteAddress = control.SingleOffsetBytes == 0 + ? address + : IAdd(address, UInt(control.SingleOffsetBytes)); + var word = Load(_uintType, LdsPointer(address, control.SingleOffsetBytes)); + var shift = ShiftLeftLogical(BitwiseAnd(byteAddress, UInt(3)), UInt(3)); + var packed = ShiftRightLogical(word, shift); + var signedByte = _module.AddInstruction( + SpirvOp.BitFieldSExtract, + _intType, + Bitcast(_intType, packed), + UInt(0), + UInt(8)); + StoreV(instruction.Destinations[0].Value, Bitcast(_uintType, signedByte)); + return true; + } case "DsReadB64": case "DsReadB96": case "DsReadB128": @@ -1798,6 +1868,27 @@ public static partial class Gen5SpirvTranslator out string error) { error = string.Empty; + if (instruction.Opcode is "DsMinF32" or "DsMaxF32") + { + if (instruction.Sources.Count < 3) + { + error = $"missing LDS operands for {instruction.Opcode}"; + return false; + } + + var floatAddress = GetRawSource(instruction, 0); + var floatPointer = LdsPointer(floatAddress, control.SingleOffsetBytes); + EmitExecConditional(() => + EmitDataShareFloatAtomic( + floatPointer, + GetRawSource(instruction, 1), + GetRawSource(instruction, 2), + instruction.Opcode == "DsMaxF32", + scope: 2, + semantics: 0x108)); + return true; + } + var atomicOp = instruction.Opcode switch { "DsAddU32" or "DsAddRtnU32" => SpirvOp.AtomicIAdd, @@ -1845,6 +1936,124 @@ public static partial class Gen5SpirvTranslator return true; } + private void EmitDataShareFloatAtomic( + uint pointer, + uint data, + uint compare, + bool maxValue, + uint scope, + uint semantics) + { + var preheader = _module.AllocateId(); + var header = _module.AllocateId(); + var continueLabel = _module.AllocateId(); + var mergeLabel = _module.AllocateId(); + var exchanged = _module.AllocateId(); + + _module.AddStatement(SpirvOp.Branch, preheader); + _module.AddLabel(preheader); + var initial = _module.AddInstruction( + SpirvOp.AtomicLoad, + _uintType, + pointer, + UInt(scope), + UInt(semantics)); + _module.AddStatement(SpirvOp.Branch, header); + + _module.AddLabel(header); + var observed = _module.AddInstruction( + SpirvOp.Phi, + _uintType, + initial, + preheader, + exchanged, + continueLabel); + var observedFloat = Bitcast(_floatType, observed); + var compareFloat = Bitcast(_floatType, compare); + var replace = _module.AddInstruction( + maxValue ? SpirvOp.FOrdGreaterThan : SpirvOp.FOrdLessThan, + _boolType, + maxValue ? observedFloat : compareFloat, + maxValue ? compareFloat : observedFloat); + var next = _module.AddInstruction(SpirvOp.Select, _uintType, replace, data, observed); + + _module.AddStatement( + SpirvOp.AtomicCompareExchange, + _uintType, + exchanged, + pointer, + UInt(scope), + UInt(semantics), + UInt((semantics & ~0x8u) | 0x2u), + next, + observed); + var success = _module.AddInstruction(SpirvOp.IEqual, _boolType, exchanged, observed); + _module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0); + _module.AddStatement(SpirvOp.BranchConditional, success, mergeLabel, continueLabel); + _module.AddLabel(continueLabel); + _module.AddStatement(SpirvOp.Branch, header); + _module.AddLabel(mergeLabel); + } + + private uint EmitBufferFloatAtomic( + uint pointer, + uint value, + bool maxValue, + uint scope, + uint semantics) + { + var preheader = _module.AllocateId(); + var header = _module.AllocateId(); + var continueLabel = _module.AllocateId(); + var mergeLabel = _module.AllocateId(); + var exchanged = _module.AllocateId(); + + _module.AddStatement(SpirvOp.Branch, preheader); + _module.AddLabel(preheader); + var initial = _module.AddInstruction( + SpirvOp.AtomicLoad, + _uintType, + pointer, + UInt(scope), + UInt(semantics)); + _module.AddStatement(SpirvOp.Branch, header); + + _module.AddLabel(header); + var observed = _module.AddInstruction( + SpirvOp.Phi, + _uintType, + initial, + preheader, + exchanged, + continueLabel); + var observedFloat = Bitcast(_floatType, observed); + var valueFloat = Bitcast(_floatType, value); + var replace = _module.AddInstruction( + maxValue ? SpirvOp.FOrdGreaterThan : SpirvOp.FOrdLessThan, + _boolType, + valueFloat, + observedFloat); + var next = _module.AddInstruction(SpirvOp.Select, _uintType, replace, value, observed); + + _module.AddStatement( + SpirvOp.AtomicCompareExchange, + _uintType, + exchanged, + pointer, + UInt(scope), + UInt(semantics), + UInt((semantics & ~0x8u) | 0x2u), + next, + observed); + var success = _module.AddInstruction(SpirvOp.IEqual, _boolType, exchanged, observed); + _module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0); + _module.AddStatement(SpirvOp.BranchConditional, success, mergeLabel, continueLabel); + _module.AddLabel(continueLabel); + _module.AddStatement(SpirvOp.Branch, header); + _module.AddLabel(mergeLabel); + return observed; + } + // Maps the AMD atomic-op name suffix shared by buffer/image atomics to a SPIR-V opcode. // Inc/Dec approximate the AMD wrap-clamp semantics (MEM = tmp >= DATA ? 0 : tmp + 1), // which is exact for the common 0xFFFFFFFF clamp operand. @@ -1926,6 +2135,11 @@ public static partial class Gen5SpirvTranslator return false; } + if (_perVertexAttributes.Contains(interpolation.Attribute)) + { + return TryEmitInterpolationParameter(instruction, interpolation, input, destination, out error); + } + var vector = Load(_vec4Type, input); var component = _module.AddInstruction( SpirvOp.CompositeExtract, @@ -1979,6 +2193,7 @@ public static partial class Gen5SpirvTranslator int bindingIndex; uint stride; uint descriptorWord3; + uint descriptorFormat; { // The dense buffer, its stride and its format come from the specialization. if (!TryResolveLayoutBuffer(instruction.Pc, out bindingIndex, out var specialized)) @@ -1988,6 +2203,7 @@ public static partial class Gen5SpirvTranslator } stride = UInt(specialized.PackedStride & 0x3FFF); + descriptorFormat = specialized.DescriptorFormat; descriptorWord3 = UInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF)); } @@ -2012,7 +2228,70 @@ public static partial class Gen5SpirvTranslator if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal)) { - if (!TryGetAtomicOp(instruction.Opcode["BufferAtomic".Length..], out var atomicOp)) + var atomicSuffix = instruction.Opcode["BufferAtomic".Length..]; + if (atomicSuffix == "OrX2") + { + EmitExecConditional(() => + { + var secondAddress = IAdd(dwordAddress, UInt(1)); + var inRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + IsBufferWordInRange(bindingIndex, dwordAddress), + IsBufferWordInRange(bindingIndex, secondAddress)); + EmitConditional(inRange, () => + { + var originalLow = EmitAtomic( + SpirvOp.AtomicOr, + _uintType, + BufferWordPointer(bindingIndex, dwordAddress), + scope: 1, + semantics: 0x48, + value: () => LoadV(control.VectorData), + comparator: () => UInt(0)); + var originalHigh = EmitAtomic( + SpirvOp.AtomicOr, + _uintType, + BufferWordPointer(bindingIndex, secondAddress), + scope: 1, + semantics: 0x48, + value: () => LoadV(control.VectorData + 1), + comparator: () => UInt(0)); + if (control.Glc) + { + StoreV(control.VectorData, originalLow); + StoreV(control.VectorData + 1, originalHigh); + } + }); + }); + + return true; + } + + if (atomicSuffix is "Fmin" or "Fmax") + { + EmitExecConditional(() => + { + var inRange = IsBufferWordInRange(bindingIndex, dwordAddress); + EmitConditional(inRange, () => + { + var original = EmitBufferFloatAtomic( + BufferWordPointer(bindingIndex, dwordAddress), + LoadV(control.VectorData), + maxValue: atomicSuffix == "Fmax", + scope: 1, + semantics: 0x48); + if (control.Glc) + { + StoreV(control.VectorData, original); + } + }); + }); + + return true; + } + + if (!TryGetAtomicOp(atomicSuffix, out var atomicOp)) { error = $"unsupported buffer opcode {instruction.Opcode}"; return false; @@ -2041,6 +2320,25 @@ public static partial class Gen5SpirvTranslator return true; } + if (instruction.Opcode is "BufferStoreFormatX" or "BufferStoreFormatXy" or + "BufferStoreFormatXyz" or "BufferStoreFormatXyzw") + { + if (descriptorFormat == 0) + { + return true; + } + + if (!Gfx10UnifiedFormat.TryDecode(descriptorFormat, out _, out _)) + { + error = $"unsupported buffer store format {descriptorFormat}"; + return false; + } + + EmitExecConditional(() => TryEmitBufferFormatStore( + bindingIndex, byteAddress, control, descriptorWord3, descriptorFormat)); + return true; + } + if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal) || instruction.Opcode.StartsWith("BufferStoreFormat", StringComparison.Ordinal) || instruction.Opcode.StartsWith("TBufferStoreFormat", StringComparison.Ordinal) || @@ -2049,7 +2347,7 @@ public static partial class Gen5SpirvTranslator { EmitExecConditional(() => { - if (control.Typed && TryEmitTypedBufferFormatStore(bindingIndex, byteAddress, control, descriptorWord3)) + if (control.Typed && TryEmitBufferFormatStore(bindingIndex, byteAddress, control, descriptorWord3, control.TypedFormat)) { return; } @@ -2265,15 +2563,16 @@ public static partial class Gen5SpirvTranslator return true; } - // A typed store converts each register with the format's number format and places + // A formatted store converts each register with the selected number format and places // the bits at the component's offset; all transferred components are stored or dropped. - private bool TryEmitTypedBufferFormatStore( + private bool TryEmitBufferFormatStore( int bindingIndex, uint byteAddress, Gen5BufferMemoryControl control, - uint descriptorWord3) + uint descriptorWord3, + uint unifiedFormat) { - if (!Gfx10UnifiedFormat.TryDecode(control.TypedFormat, out var dataFormat, out var numberFormat)) + if (!Gfx10UnifiedFormat.TryDecode(unifiedFormat, out var dataFormat, out var numberFormat)) { return false; } @@ -3089,7 +3388,8 @@ public static partial class Gen5SpirvTranslator private static bool UsesSampler(string opcode) => opcode.StartsWith("ImageSample", StringComparison.Ordinal) || - opcode.StartsWith("ImageGather", StringComparison.Ordinal); + opcode.StartsWith("ImageGather", StringComparison.Ordinal) || + opcode == "ImageGetLod"; private bool TryEmitVertexInputFetch( Gen5BufferMemoryControl control, @@ -3108,25 +3408,41 @@ public static partial class Gen5SpirvTranslator for (uint component = 0; component < control.DwordCount; component++) { uint raw; - if (component >= input.ComponentCount) + var selector = (input.DestinationSelect >> (int)(component * 3)) & 0x7u; + if (selector == 0) { - // Formatted buffer loads return zero for components that - // are not present in the resource format. raw = UInt(0); } - else + else if (selector == 1) { + raw = UInt(input.NumberFormat is 4u or 5u ? 1u : 0x3F80_0000u); + } + else if (selector is >= 4u and <= 7u) + { + var sourceComponent = selector - 4u; + if (sourceComponent >= input.ComponentCount) + { + error = + $"vertex input destination selector exceeds source components: selector={selector} components={input.ComponentCount}"; + return false; + } + var value = input.ComponentCount == 1 ? loaded : _module.AddInstruction( SpirvOp.CompositeExtract, input.ComponentType, loaded, - component); + sourceComponent); raw = input.ComponentKind == VertexInputComponentKind.Uint ? value : Bitcast(_uintType, value); } + else + { + error = $"unsupported vertex input destination selector={selector}"; + return false; + } StoreV(control.VectorData + component, raw); } @@ -3152,10 +3468,11 @@ public static partial class Gen5SpirvTranslator var caseError = string.Empty; for (var index = 0; index < elements.Count && emitted; index++) { - var element = elements[index]; + var elementCase = elements[index]; EmitConditional(_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt((uint)index)), () => { - if (!TryResolveLayoutImage(instruction, image, out var caseResource, out var caseImageObject, out var caseDstSelect, out caseError, element)) + if (!TryResolveLayoutImage(instruction, image, out var caseResource, out var caseImageObject, out var caseDstSelect, out caseError, + elementCase)) { emitted = false; return; @@ -3209,20 +3526,21 @@ public static partial class Gen5SpirvTranslator if (instruction.Opcode == "ImageGetResinfo") { var sizeComponentCount = ImageCoordinateComponentCount(resource); - var queryImage = resource.IsStorage - ? imageObject - : _module.AddInstruction( - SpirvOp.Image, - resource.ImageType, - imageObject); + var queryImage = imageObject; var size = _module.AddInstruction( - resource.IsStorage + resource.IsStorage || resource.Multisampled ? SpirvOp.ImageQuerySize : SpirvOp.ImageQuerySizeLod, - _module.TypeVector(_intType, sizeComponentCount), - resource.IsStorage + ImageIntegerCoordinateType(sizeComponentCount), + resource.IsStorage || resource.Multisampled ? [queryImage] - : [queryImage, UInt(0)]); + : [queryImage, LoadImageIntegerAddress(image, 0)]); + var levels = !resource.IsStorage && !resource.Multisampled + ? _module.AddInstruction( + SpirvOp.ImageQueryLevels, + _uintType, + queryImage) + : UInt(1); uint outputIndex = 0; for (uint component = 0; component < 4; component++) { @@ -3234,16 +3552,22 @@ public static partial class Gen5SpirvTranslator uint value; if (component < sizeComponentCount) { - var signedValue = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - size, - component); + var signedValue = sizeComponentCount == 1 + ? size + : _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + size, + component); value = Bitcast(_uintType, signedValue); } + else if (component == 3) + { + value = levels; + } else { - value = UInt(1); + value = UInt(0); } StoreV(image.VectorData + outputIndex++, value); @@ -3252,6 +3576,39 @@ public static partial class Gen5SpirvTranslator return true; } + if (instruction.Opcode == "ImageGetLod") + { + var coordinateComponentCount = ImageCoordinateComponentCount(resource); + var coordinates = BuildFloatCoordinates( + image, + 0, + coordinateComponentCount, + resource); + var queried = _module.AddInstruction( + SpirvOp.ImageQueryLod, + _vec2Type, + imageObject, + coordinates); + uint outputIndex = 0; + var mask = image.Dmask != 0 ? image.Dmask : 1u; + for (uint component = 0; component < 2; component++) + { + if ((mask & (1u << (int)component)) == 0) + { + continue; + } + + var value = _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + queried, + component); + StoreV(image.VectorData + outputIndex++, Bitcast(_uintType, value)); + } + + return true; + } + if (instruction.Opcode is "ImageStore" or "ImageStoreMip") { if (!resource.IsStorage) @@ -3302,16 +3659,13 @@ public static partial class Gen5SpirvTranslator SpirvOp.CompositeConstruct, resource.VectorType, components); - var imageSize = _module.AddInstruction( - SpirvOp.ImageQuerySize, - _module.TypeVector(_intType, coordinateComponentCount), - imageObject); - EmitBoundsCheckedImageWrite( - coordinates, - imageSize, - imageObject, - texel, - coordinateComponentCount); + texel = PackImageTexel(resource, texel); + EmitExecConditional(() => + _module.AddStatement( + SpirvOp.ImageWrite, + imageObject, + coordinates, + texel)); return true; } @@ -3334,14 +3688,9 @@ public static partial class Gen5SpirvTranslator var signed = resource.ComponentKind == ImageComponentKind.Sint; var coordinateComponentCount = ImageCoordinateComponentCount(resource); - var atomicImageSize = _module.AddInstruction( - SpirvOp.ImageQuerySize, - _module.TypeVector(_intType, coordinateComponentCount), - imageObject); - var coordinates = BuildClampedIntegerCoordinates( + var coordinates = BuildIntegerCoordinates( image, 0, - atomicImageSize, coordinateComponentCount); EmitExecConditional(() => { @@ -3388,14 +3737,9 @@ public static partial class Gen5SpirvTranslator { var coordinateComponentCount = ImageCoordinateComponentCount(resource); - var imageSize = _module.AddInstruction( - SpirvOp.ImageQuerySize, - _module.TypeVector(_intType, coordinateComponentCount), - imageObject); - var coordinates = BuildClampedIntegerCoordinates( + var coordinates = BuildIntegerCoordinates( image, 0, - imageSize, coordinateComponentCount); sampled = _module.AddInstruction( SpirvOp.ImageRead, @@ -3409,24 +3753,36 @@ public static partial class Gen5SpirvTranslator var fetchedImage = imageObject; var coordinateComponentCount = ImageCoordinateComponentCount(resource); - var imageSize = _module.AddInstruction( - SpirvOp.ImageQuerySizeLod, - _module.TypeVector(_intType, coordinateComponentCount), - fetchedImage, - mipLevel); - var coordinates = BuildClampedIntegerCoordinates( + var coordinates = BuildIntegerCoordinates( image, 0, - imageSize, coordinateComponentCount); - sampled = _module.AddInstruction( - SpirvOp.ImageFetch, - resource.VectorType, - fetchedImage, - coordinates, - 2, - mipLevel); + if (resource.Multisampled) + { + var sample = LoadImageIntegerAddress( + image, + (int)coordinateComponentCount); + sampled = _module.AddInstruction( + SpirvOp.ImageFetch, + resource.VectorType, + fetchedImage, + coordinates, + 0x40, + sample); + } + else + { + sampled = _module.AddInstruction( + SpirvOp.ImageFetch, + resource.VectorType, + fetchedImage, + coordinates, + 2, + mipLevel); + } } + + sampled = UnpackImageTexel(resource, sampled); } else if (instruction.Opcode.StartsWith( "ImageSample", @@ -3435,9 +3791,12 @@ public static partial class Gen5SpirvTranslator var hasOffset = instruction.Opcode.EndsWith("O", StringComparison.Ordinal); var hasCompare = - instruction.Opcode.Contains("SampleC", StringComparison.Ordinal); + instruction.Opcode.Contains("SampleC", StringComparison.Ordinal) && + !instruction.Opcode.StartsWith("ImageSampleCd", StringComparison.Ordinal); var hasGradients = - instruction.Opcode.Contains("SampleD", StringComparison.Ordinal); + instruction.Opcode.Contains("SampleD", StringComparison.Ordinal) || + instruction.Opcode.Contains("SampleCd", StringComparison.Ordinal) || + instruction.Opcode.Contains("SampleCCd", StringComparison.Ordinal); var hasZeroLod = instruction.Opcode.Contains("Lz", StringComparison.Ordinal); var hasLod = !hasZeroLod && @@ -3445,6 +3804,12 @@ public static partial class Gen5SpirvTranslator var hasBias = instruction.Opcode.Contains("SampleB", StringComparison.Ordinal); + if (hasCompare && resource.ConversionFormat != GuestImageFormat.Invalid) + { + error = "image sample uses depth comparison with a packed integer image"; + return false; + } + // RDNA MIMG address operands are ordered as // {offset}{bias/lod}{z-compare}{derivatives}{body}. The old // lowering treated SAMPLE_D as body-first and consequently @@ -3489,13 +3854,15 @@ public static partial class Gen5SpirvTranslator ? BuildFloatCoordinates( image, addressCursor, - spatialComponentCount) + spatialComponentCount, + resource) : 0u; var gradientY = hasGradients ? BuildFloatCoordinates( image, addressCursor + (int)spatialComponentCount, - spatialComponentCount) + spatialComponentCount, + resource) : 0u; if (hasGradients) { @@ -3505,15 +3872,21 @@ public static partial class Gen5SpirvTranslator var coordinates = BuildFloatCoordinates( image, addressCursor, - coordinateComponentCount); - var explicitLod = hasGradients || hasZeroLod || hasLod; + coordinateComponentCount, + resource); + // Non-pixel samples require explicit derivatives or a level of detail. + // Use level zero when the instruction supplies neither. + var explicitLod = hasGradients || hasZeroLod || hasLod || + _stage != Gen5SpirvStage.Pixel; var lod = hasZeroLod ? Float(0) : hasLod ? LoadImageFloatAddress( image, addressCursor + (int)coordinateComponentCount) - : lodOrBias; + : explicitLod + ? Float(0) + : lodOrBias; if (hasOffset) { // Vulkan before maintenance8 forbids the dynamic Offset @@ -3584,6 +3957,7 @@ public static partial class Gen5SpirvTranslator : SpirvOp.ImageSampleImplicitLod, resource.VectorType, [.. operands]); + sampled = UnpackImageTexel(resource, sampled); } } else if (instruction.Opcode.StartsWith( @@ -3594,6 +3968,14 @@ public static partial class Gen5SpirvTranslator instruction.Opcode.EndsWith("O", StringComparison.Ordinal); var hasCompare = instruction.Opcode.Contains("Gather4C", StringComparison.Ordinal); + var gatherHorizontal = + instruction.Opcode == "ImageGather4H"; + + if (hasCompare && resource.ConversionFormat != GuestImageFormat.Invalid) + { + error = "image gather uses depth comparison with a packed integer image"; + return false; + } var spatialComponentCount = ImageSpatialComponentCount(resource); var coordinateComponentCount = @@ -3623,7 +4005,35 @@ public static partial class Gen5SpirvTranslator var coordinates = BuildFloatCoordinates( image, addressCursor, - coordinateComponentCount); + coordinateComponentCount, + resource); + + if (resource.Dimension == SpirvImageDim.Dim1D) + { + var levelZero = + instruction.Opcode.Contains("Lz", StringComparison.Ordinal); + if (resource.Arrayed || + hasCompare || + hasOffset || + gatherHorizontal || + !levelZero) + { + error = resource.Arrayed + ? "unsupported 1D-array image gather" + : "unsupported 1D image gather variant"; + return false; + } + + sampled = EmitOneDimensionalGatherLz( + image, + resource, + imageObject, + coordinates); + sampled = UnpackImageGather(resource, image.Dmask, sampled); + writeAllComponents = true; + goto GatherComplete; + } + var operands = new List { imageObject, @@ -3640,10 +4050,13 @@ public static partial class Gen5SpirvTranslator else { uint component = 0; - while (component < 3 && - (image.Dmask & (1u << (int)component)) == 0) + if (resource.ConversionFormat == GuestImageFormat.Invalid) { - component++; + while (component < 3 && + (image.Dmask & (1u << (int)component)) == 0) + { + component++; + } } operands.Add(UInt(component)); @@ -3654,13 +4067,29 @@ public static partial class Gen5SpirvTranslator operands.Add(0x10u); operands.Add(offset); } + else if (gatherHorizontal) + { + if (resource.Dimension == SpirvImageDim.Dim1D) + { + error = "unsupported 1D horizontal image gather"; + return false; + } + + operands.Add(0x20u); + operands.Add(BuildHorizontalGatherOffsets()); + } sampled = _module.AddInstruction( hasCompare ? SpirvOp.ImageDrefGather : SpirvOp.ImageGather, resource.VectorType, [.. operands]); + if (!hasCompare) + { + sampled = UnpackImageGather(resource, image.Dmask, sampled); + } writeAllComponents = true; + GatherComplete:; } else { @@ -3741,18 +4170,289 @@ public static partial class Gen5SpirvTranslator return true; } + private uint EmitOneDimensionalGatherLz( + Gen5ImageControl image, + SpirvImageResource resource, + uint sampledImage, + uint coordinate) + { + var imageValue = Load(resource.ImageType, resource.Variable); + var width = _module.AddInstruction( + SpirvOp.ImageQuerySizeLod, + _intType, + imageValue, + _module.Constant(_intType, 0)); + var widthFloat = _module.AddInstruction( + SpirvOp.ConvertSToF, + _floatType, + width); + var centered = _module.AddInstruction( + SpirvOp.FSub, + _floatType, + _module.AddInstruction( + SpirvOp.FMul, + _floatType, + coordinate, + widthFloat), + Float(0.5f)); + var left = Ext(8, _floatType, centered); + + uint component = 0; + if (resource.ConversionFormat == GuestImageFormat.Invalid) + { + while (component < 3 && + (image.Dmask & (1u << (int)component)) == 0) + { + component++; + } + } + + var values = new uint[2]; + for (var index = 0; index < values.Length; index++) + { + var sampleCoordinate = _module.AddInstruction( + SpirvOp.FDiv, + _floatType, + _module.AddInstruction( + SpirvOp.FAdd, + _floatType, + left, + Float(index == 0 ? 0.5f : 1.5f)), + widthFloat); + var texel = _module.AddInstruction( + SpirvOp.ImageSampleExplicitLod, + resource.VectorType, + sampledImage, + sampleCoordinate, + 2u, + Float(0)); + values[index] = _module.AddInstruction( + SpirvOp.CompositeExtract, + resource.ComponentType, + texel, + component); + } + + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + resource.VectorType, + values[0], + values[1], + values[1], + values[0]); + } + + private static bool TryGetPackedImageConversion( + SpirvImageResource resource, + out int componentCount, + out ReadOnlySpan componentBits, + out ReadOnlySpan componentOffsets) + { + if (resource.ConversionFormat == GuestImageFormat.Format11x2x10Uint) + { + componentCount = 3; + componentBits = [11u, 11u, 10u]; + componentOffsets = [0u, 11u, 22u]; + return true; + } + + componentCount = 0; + componentBits = default; + componentOffsets = default; + return false; + } + + private uint UnpackImageTexel( + SpirvImageResource resource, + uint texel) + { + if (!TryGetPackedImageConversion( + resource, + out var componentCount, + out var componentBits, + out var componentOffsets)) + { + return texel; + } + + var packed = _module.AddInstruction( + SpirvOp.CompositeExtract, + _uintType, + texel, + 0u); + var components = new uint[4]; + for (var component = 0; component < componentCount; component++) + { + components[component] = _module.AddInstruction( + SpirvOp.BitFieldUExtract, + _uintType, + packed, + UInt(componentOffsets[component]), + UInt(componentBits[component])); + } + + for (var component = componentCount; component < components.Length; component++) + { + components[component] = components[component % componentCount]; + } + + var selected = new uint[4]; + for (var component = 0; component < selected.Length; component++) + { + var selector = (resource.ShaderSwizzle >> (component * 3)) & 7u; + selected[component] = selector switch + { + 1u => UInt(1), + >= 4u => components[(selector - 4u) % (uint)componentCount], + _ => UInt(0), + }; + } + + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + resource.VectorType, + selected); + } + + private uint UnpackImageGather( + SpirvImageResource resource, + uint dmask, + uint gathered) + { + if (!TryGetPackedImageConversion( + resource, + out var componentCount, + out var componentBits, + out var componentOffsets)) + { + return gathered; + } + + uint component = 0; + while (component < 3 && (dmask & (1u << (int)component)) == 0) + { + component++; + } + + var selector = (resource.ShaderSwizzle >> ((int)component * 3)) & 7u; + if (selector < 4u) + { + var value = selector == 1u ? UInt(1) : UInt(0); + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + resource.VectorType, + value, + value, + value, + value); + } + + var physical = (selector - 4u) % (uint)componentCount; + var values = new uint[4]; + for (var lane = 0; lane < values.Length; lane++) + { + var packed = _module.AddInstruction( + SpirvOp.CompositeExtract, + _uintType, + gathered, + (uint)lane); + values[lane] = _module.AddInstruction( + SpirvOp.BitFieldUExtract, + _uintType, + packed, + UInt(componentOffsets[(int)physical]), + UInt(componentBits[(int)physical])); + } + + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + resource.VectorType, + values); + } + + private uint PackImageTexel( + SpirvImageResource resource, + uint texel) + { + if (!TryGetPackedImageConversion( + resource, + out var componentCount, + out var componentBits, + out var componentOffsets)) + { + return texel; + } + + var packed = UInt(0); + for (var component = 0; component < componentCount; component++) + { + var value = _module.AddInstruction( + SpirvOp.CompositeExtract, + _uintType, + texel, + (uint)component); + var maximum = (1u << (int)componentBits[component]) - 1u; + var within = _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + value, + UInt(maximum)); + var clamped = _module.AddInstruction( + SpirvOp.Select, + _uintType, + within, + value, + UInt(maximum)); + var shifted = componentOffsets[component] == 0 + ? clamped + : ShiftLeftLogical(clamped, UInt(componentOffsets[component])); + packed = BitwiseOr(packed, shifted); + } + + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + resource.VectorType, + packed, + UInt(0), + UInt(0), + UInt(0)); + } + + private uint BuildHorizontalGatherOffsets() + { + var vec2Int = _module.TypeVector(_intType, 2); + var offsets = new uint[4]; + for (var index = 0; index < offsets.Length; index++) + { + offsets[index] = _module.ConstantComposite( + vec2Int, + _module.Constant(_intType, unchecked((uint)(index - 1))), + _module.Constant(_intType, 0)); + } + + return _module.ConstantComposite( + _module.TypeArray(vec2Int, 4), + offsets); + } + private static uint ImageSpatialComponentCount( SpirvImageResource resource) => ImageSpatialComponentCountOf(resource.Dimension); private static uint ImageCoordinateComponentCount( SpirvImageResource resource) => - resource.Arrayed ? 3u : ImageSpatialComponentCount(resource); + ImageSpatialComponentCount(resource) + (resource.Arrayed ? 1u : 0u); + + private uint ImageIntegerCoordinateType(uint componentCount) => + componentCount == 1 + ? _intType + : _module.TypeVector(_intType, componentCount); private uint BuildFloatCoordinates( Gen5ImageControl image, int start, - uint componentCount) + uint componentCount, + SpirvImageResource resource) { var components = new uint[checked((int)componentCount)]; for (var component = 0; component < components.Length; component++) @@ -3762,6 +4462,24 @@ public static partial class Gen5SpirvTranslator start + component); } + if (resource.Cube && components.Length >= 2) + { + components[0] = _module.AddInstruction(SpirvOp.FSub, _floatType, components[0], Float(1)); + components[1] = _module.AddInstruction(SpirvOp.FSub, _floatType, components[1], Float(1)); + if (components.Length >= 3) + { + var guestLayer = _module.AddInstruction(SpirvOp.ConvertFToU, _uintType, components[2]); + var padding = ShiftLeftLogical(ShiftRightLogical(guestLayer, UInt(3)), UInt(1)); + var hostLayer = _module.AddInstruction(SpirvOp.ISub, _uintType, guestLayer, padding); + components[2] = _module.AddInstruction(SpirvOp.ConvertUToF, _floatType, hostLayer); + } + } + + if (componentCount == 1) + { + return components[0]; + } + return _module.AddInstruction( SpirvOp.CompositeConstruct, _module.TypeVector(_floatType, componentCount), @@ -3883,133 +4601,17 @@ public static partial class Gen5SpirvTranslator LoadImageIntegerAddress(image, start + component)); } - return _module.AddInstruction( - SpirvOp.CompositeConstruct, - _module.TypeVector(_intType, componentCount), - components); - } - - private uint BuildClampedIntegerCoordinates( - Gen5ImageControl image, - int start, - uint imageSize, - uint componentCount) - { - var components = new uint[checked((int)componentCount)]; - for (var component = 0; component < components.Length; component++) + if (componentCount == 1) { - components[component] = ClampSignedCoordinate( - Bitcast( - _intType, - LoadImageIntegerAddress(image, start + component)), - _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - imageSize, - (uint)component)); + return components[0]; } return _module.AddInstruction( SpirvOp.CompositeConstruct, - _module.TypeVector(_intType, componentCount), + ImageIntegerCoordinateType(componentCount), components); } - private uint ClampSignedCoordinate(uint value, uint extent) - { - var zero = _module.Constant(_intType, 0); - var max = _module.AddInstruction( - SpirvOp.ISub, - _intType, - extent, - _module.Constant(_intType, 1)); - var belowZero = _module.AddInstruction( - SpirvOp.SLessThan, - _boolType, - value, - zero); - var atLeastZero = _module.AddInstruction( - SpirvOp.Select, - _intType, - belowZero, - zero, - value); - var aboveMax = _module.AddInstruction( - SpirvOp.SGreaterThan, - _boolType, - atLeastZero, - max); - return _module.AddInstruction( - SpirvOp.Select, - _intType, - aboveMax, - max, - atLeastZero); - } - - private void EmitBoundsCheckedImageWrite( - uint coordinates, - uint imageSize, - uint imageObject, - uint texel, - uint coordinateComponentCount) - { - var zero = _module.Constant(_intType, 0); - var inRange = Load(_boolType, _exec); - for (uint component = 0; - component < coordinateComponentCount; - component++) - { - var coordinate = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - coordinates, - component); - var extent = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - imageSize, - component); - var nonNegative = _module.AddInstruction( - SpirvOp.SGreaterThanEqual, - _boolType, - coordinate, - zero); - var belowExtent = _module.AddInstruction( - SpirvOp.SLessThan, - _boolType, - coordinate, - extent); - var componentInRange = _module.AddInstruction( - SpirvOp.LogicalAnd, - _boolType, - nonNegative, - belowExtent); - inRange = _module.AddInstruction( - SpirvOp.LogicalAnd, - _boolType, - inRange, - componentInRange); - } - - var writeLabel = _module.AllocateId(); - var mergeLabel = _module.AllocateId(); - _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); - _module.AddStatement( - SpirvOp.BranchConditional, - inRange, - writeLabel, - mergeLabel); - _module.AddLabel(writeLabel); - _module.AddStatement( - SpirvOp.ImageWrite, - imageObject, - coordinates, - texel); - _module.AddStatement(SpirvOp.Branch, mergeLabel); - _module.AddLabel(mergeLabel); - } - private uint BuildImageOffset( Gen5ImageControl image, int component, @@ -4030,6 +4632,11 @@ public static partial class Gen5SpirvTranslator UInt(6)); } + if (componentCount == 1) + { + return components[0]; + } + return _module.AddInstruction( SpirvOp.CompositeConstruct, _module.TypeVector(_intType, componentCount), @@ -4047,12 +4654,20 @@ public static partial class Gen5SpirvTranslator var coordinateComponentCount = ImageCoordinateComponentCount(resource); var spatialIntegerType = - _module.TypeVector(_intType, spatialComponentCount); + spatialComponentCount == 1 + ? _intType + : _module.TypeVector(_intType, spatialComponentCount); var spatialFloatType = - _module.TypeVector(_floatType, spatialComponentCount); + spatialComponentCount == 1 + ? _floatType + : _module.TypeVector(_floatType, spatialComponentCount); var queryComponentCount = resource.Arrayed ? coordinateComponentCount : spatialComponentCount; + var queryIntegerType = + queryComponentCount == 1 + ? _intType + : _module.TypeVector(_intType, queryComponentCount); var image = _module.AddInstruction( SpirvOp.Image, resource.ImageType, @@ -4074,18 +4689,35 @@ public static partial class Gen5SpirvTranslator signedLod); var size = _module.AddInstruction( SpirvOp.ImageQuerySizeLod, - _module.TypeVector(_intType, queryComponentCount), + queryIntegerType, image, clampedLod); if (resource.Arrayed) { - size = _module.AddInstruction( - SpirvOp.VectorShuffle, - spatialIntegerType, - size, - size, - 0u, - 1u); + if (spatialComponentCount == 1) + { + size = _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + size, + 0u); + } + else + { + var spatialSizeComponents = + new uint[checked((int)spatialComponentCount)]; + for (uint component = 0; + component < spatialComponentCount; + component++) + { + spatialSizeComponents[component] = component; + } + + size = _module.AddInstruction( + SpirvOp.VectorShuffle, + spatialIntegerType, + [size, size, .. spatialSizeComponents]); + } } var sizeFloat = _module.AddInstruction( @@ -4110,23 +4742,29 @@ public static partial class Gen5SpirvTranslator normalizedOffset); } + var arrayOffsetComponents = + new uint[checked((int)coordinateComponentCount)]; + for (uint component = 0; + component < spatialComponentCount; + component++) + { + arrayOffsetComponents[component] = + spatialComponentCount == 1 + ? normalizedOffset + : _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + normalizedOffset, + component); + } + arrayOffsetComponents[coordinateComponentCount - 1] = Float(0); var arrayOffset = _module.AddInstruction( SpirvOp.CompositeConstruct, - _vec3Type, - _module.AddInstruction( - SpirvOp.CompositeExtract, - _floatType, - normalizedOffset, - 0u), - _module.AddInstruction( - SpirvOp.CompositeExtract, - _floatType, - normalizedOffset, - 1u), - Float(0)); + _module.TypeVector(_floatType, coordinateComponentCount), + arrayOffsetComponents); return _module.AddInstruction( SpirvOp.FAdd, - _vec3Type, + _module.TypeVector(_floatType, coordinateComponentCount), coordinates, arrayOffset); } @@ -4356,6 +4994,7 @@ public static partial class Gen5SpirvTranslator { if (export.Target != 12) { + EmitAuxPositionExport(instruction, export); return true; } @@ -4387,6 +5026,10 @@ public static partial class Gen5SpirvTranslator SpirvOp.CompositeConstruct, _vec4Type, components); + if (export.Target == 12 && _request.ClipSpace.Enabled) + { + outputValue = ConvertPositionToClipSpace(outputValue); + } if (_request.Program.Address == 0x0000000500780000ul && export.Target is >= 32 and < 36 && Environment.GetEnvironmentVariable( @@ -4410,6 +5053,339 @@ public static partial class Gen5SpirvTranslator return true; } + private readonly record struct PositionExportComponent( + uint ClipDistance, + uint CullDistance, + bool PointSize, + bool Layer, + bool Viewport); + + private static PositionExportComponent DecodePositionExportComponent( + uint control, + uint positionIndex, + uint component) + { + if (positionIndex == 0 || component >= 4) + { + return new(uint.MaxValue, uint.MaxValue, false, false, false); + } + + var slot = positionIndex - 1; + var vector = 3u; + for (var index = 0u; index < 3; index++) + { + if ((control & (1u << (int)(21 + index))) == 0) + { + continue; + } + + if (slot == 0) + { + vector = index; + break; + } + + slot--; + } + + if (vector == 3) + { + return new(uint.MaxValue, uint.MaxValue, false, false, false); + } + + if (vector == 0) + { + return new( + uint.MaxValue, + uint.MaxValue, + component == 0 && (control & (1u << 16)) != 0, + component == 2 && (control & (1u << 18)) != 0, + component == 2 && (control & (1u << 19)) != 0); + } + + var scalar = (vector - 1) * 4 + component; + var lower = scalar == 0 ? 0u : (1u << (int)scalar) - 1u; + var clip = control & 0xffu; + var cull = (control >> 8) & 0xffu; + return new( + (clip & (1u << (int)scalar)) != 0 + ? (uint)System.Numerics.BitOperations.PopCount(clip & lower) + : uint.MaxValue, + (cull & (1u << (int)scalar)) != 0 + ? (uint)System.Numerics.BitOperations.PopCount(cull & lower) + : uint.MaxValue, + false, + false, + false); + } + + private void DeclareAuxPositionOutputs() + { + var needPointSize = false; + var needLayer = false; + var needViewport = false; + var clipCount = 0u; + var cullCount = 0u; + + foreach (var export in _request.Program.Instructions + .Select(static instruction => instruction.Control) + .OfType() + .Where(static export => export.Target is >= 13 and < 16)) + { + var positionIndex = export.Target - 12; + for (var component = 0u; component < 4; component++) + { + if ((export.EnableMask & (1u << (int)component)) == 0) + { + continue; + } + + var output = DecodePositionExportComponent( + _request.PositionExportControl, + positionIndex, + component); + needPointSize |= output.PointSize; + needLayer |= output.Layer; + needViewport |= output.Viewport; + if (output.ClipDistance != uint.MaxValue) + { + clipCount = Math.Max(clipCount, output.ClipDistance + 1); + } + if (output.CullDistance != uint.MaxValue) + { + cullCount = Math.Max(cullCount, output.CullDistance + 1); + } + } + } + + if (needPointSize) + { + _pointSizeOutput = DeclareBuiltInOutput( + _floatType, + SpirvBuiltIn.PointSize, + "gl_PointSize"); + } + if (needLayer) + { + _module.AddCapability(SpirvCapability.ShaderLayer); + _layerOutput = DeclareBuiltInOutput( + _uintType, + SpirvBuiltIn.Layer, + "gl_Layer"); + } + if (needViewport) + { + _module.AddCapability(SpirvCapability.ShaderViewportIndex); + _viewportIndexOutput = DeclareBuiltInOutput( + _uintType, + SpirvBuiltIn.ViewportIndex, + "gl_ViewportIndex"); + } + if (clipCount != 0) + { + _module.AddCapability(SpirvCapability.ClipDistance); + _clipDistanceCount = clipCount; + _clipDistanceOutput = DeclareBuiltInOutput( + _module.TypeArray(_floatType, clipCount), + SpirvBuiltIn.ClipDistance, + "gl_ClipDistance"); + } + if (cullCount != 0) + { + _module.AddCapability(SpirvCapability.CullDistance); + _cullDistanceCount = cullCount; + _cullDistanceOutput = DeclareBuiltInOutput( + _module.TypeArray(_floatType, cullCount), + SpirvBuiltIn.CullDistance, + "gl_CullDistance"); + } + } + + private uint DeclareBuiltInOutput( + uint type, + SpirvBuiltIn builtIn, + string name) + { + var pointer = _module.TypePointer(SpirvStorageClass.Output, type); + var variable = _module.AddGlobalVariable(pointer, SpirvStorageClass.Output); + _module.AddName(variable, name); + _module.AddDecoration( + variable, + SpirvDecoration.BuiltIn, + (uint)builtIn); + _interfaces.Add(variable); + return variable; + } + + private void InitializeDistanceOutput(uint variable, uint count) + { + if (variable == 0) + { + return; + } + + var pointerType = _module.TypePointer(SpirvStorageClass.Output, _floatType); + for (var index = 0u; index < count; index++) + { + var pointer = _module.AddInstruction( + SpirvOp.AccessChain, + pointerType, + variable, + UInt(index)); + Store(pointer, Float(0f)); + } + } + + private void EmitAuxPositionExport( + Gen5ShaderInstruction instruction, + Gen5ExportControl export) + { + var positionIndex = export.Target - 12; + for (var component = 0u; component < 4; component++) + { + if ((export.EnableMask & (1u << (int)component)) == 0) + { + continue; + } + + var output = DecodePositionExportComponent( + _request.PositionExportControl, + positionIndex, + component); + if (!output.PointSize && + !output.Layer && + !output.Viewport && + output.ClipDistance == uint.MaxValue && + output.CullDistance == uint.MaxValue) + { + continue; + } + + var raw = export.Compressed + ? Bitcast( + _uintType, + LoadCompressedExportComponent(instruction, (int)component)) + : LoadV(instruction.Sources[(int)component].Value); + if (output.Layer && _layerOutput != 0) + { + StoreConditional( + _layerOutput, + BitwiseAnd(raw, UInt(0x7ff)), + _uintType); + } + if (output.Viewport && _viewportIndexOutput != 0) + { + var viewport = _module.AddInstruction( + SpirvOp.BitFieldUExtract, + _uintType, + raw, + UInt(16), + UInt(4)); + StoreConditional(_viewportIndexOutput, viewport, _uintType); + } + + var value = Bitcast(_floatType, raw); + if (output.PointSize && _pointSizeOutput != 0) + { + StoreConditional(_pointSizeOutput, value, _floatType); + } + StoreDistanceConditional( + _clipDistanceOutput, + output.ClipDistance, + value); + StoreDistanceConditional( + _cullDistanceOutput, + output.CullDistance, + value); + } + } + + private void StoreDistanceConditional( + uint variable, + uint index, + uint value) + { + if (variable == 0 || index == uint.MaxValue) + { + return; + } + + var pointer = _module.AddInstruction( + SpirvOp.AccessChain, + _module.TypePointer(SpirvStorageClass.Output, _floatType), + variable, + UInt(index)); + StoreConditional(pointer, value, _floatType); + } + + private void StoreConditional(uint variable, uint value, uint type) + { + var selected = _module.AddInstruction( + SpirvOp.Select, + type, + Load(_boolType, _exec), + value, + Load(type, variable)); + Store(variable, selected); + } + + private uint ConvertPositionToClipSpace(uint position) + { + var transform = _request.ClipSpace; + var components = new uint[4]; + for (var component = 0u; component < 4; component++) + { + components[component] = _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + position, + component); + } + + components[0] = ConvertClipCoordinate( + components[0], + transform.ScaleX, + transform.OffsetX, + transform.HalfExtentX); + components[1] = ConvertClipCoordinate( + components[1], + transform.ScaleY, + transform.OffsetY, + transform.HalfExtentY); + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + _vec4Type, + components); + } + + private uint ConvertClipCoordinate( + uint coordinate, + float scale, + float offset, + float halfExtent) + { + var window = _module.AddInstruction( + SpirvOp.FMul, + _floatType, + coordinate, + Float(scale)); + var biased = _module.AddInstruction( + SpirvOp.FAdd, + _floatType, + window, + Float(offset)); + var divided = _module.AddInstruction( + SpirvOp.FDiv, + _floatType, + biased, + Float(halfExtent)); + return _module.AddInstruction( + SpirvOp.FSub, + _floatType, + divided, + Float(1f)); + } + private bool PixelExportDebugAddressMatches() { var addressFilter = Environment.GetEnvironmentVariable( diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs index f348be6..921f0bb 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs @@ -185,6 +185,10 @@ public enum SpirvOp : ushort public enum SpirvCapability : uint { Shader = 1, + InterpolationFunction = 52, + FragmentBarycentricKhr = 5284, + ClipDistance = 32, + CullDistance = 33, SampledImageArrayDynamicIndexing = 29, StorageImageArrayDynamicIndexing = 31, SampleRateShading = 35, @@ -203,6 +207,8 @@ public enum SpirvCapability : uint GroupNonUniformVote = 62, GroupNonUniformBallot = 64, GroupNonUniformShuffle = 65, + ShaderLayer = 5253, + ShaderViewportIndex = 5254, RuntimeDescriptorArray = 5302, PhysicalStorageBufferAddresses = 5347, } @@ -243,6 +249,7 @@ public enum SpirvDecoration : uint BuiltIn = 11, NoPerspective = 13, Flat = 14, + PerVertexKhr = 5285, Location = 30, Binding = 33, DescriptorSet = 34, @@ -254,9 +261,16 @@ public enum SpirvDecoration : uint public enum SpirvBuiltIn : uint { Position = 0, + PointSize = 1, + ClipDistance = 3, + CullDistance = 4, + Layer = 9, + ViewportIndex = 10, VertexIndex = 42, InstanceIndex = 43, FragCoord = 15, + BaryCoordKhr = 5286, + BaryCoordNoPerspKhr = 5287, FrontFacing = 17, WorkgroupId = 26, LocalInvocationId = 27, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 39ac7af..8929e2b 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -829,6 +829,7 @@ public static partial class Gen5ShaderTranslator 0x43 => "VMovrelsB32", 0x44 => "VMovrelsdB32", 0x48 => "VMovrelsd2B32", + 0x56 => "VRsqF16", _ => string.Empty, }; @@ -1009,6 +1010,7 @@ public static partial class Gen5ShaderTranslator 0xED => "VCmpNeqF16", 0xEE => "VCmpNltF16", 0xEF => "VCmpTruF16", + 0xF5 => "VCmpxNeU64", 0xF8 => "VCmpxUF16", 0xF9 => "VCmpxNgeF16", 0xFA => "VCmpxNlgF16", @@ -1087,6 +1089,8 @@ public static partial class Gen5ShaderTranslator 0x16A => "VMulHiU32", 0x16B => "VMulLoI32", 0x16C => "VMulHiI32", + 0x303 => "VAddNcU16", + 0x34B => "VFmaF16", 0x360 => "VReadlaneB32", 0x361 => "VWritelaneB32", 0x362 => "VLdexpF32", @@ -1193,6 +1197,8 @@ public static partial class Gen5ShaderTranslator 0x0E => "DsWrite2B32", 0x0F => "DsWrite2St64B32", 0x10 => "DsCmpstB32", + 0x12 => "DsMinF32", + 0x13 => "DsMaxF32", 0x20 => "DsAddRtnU32", 0x21 => "DsSubRtnU32", 0x23 => "DsIncRtnU32", @@ -1210,6 +1216,7 @@ public static partial class Gen5ShaderTranslator 0x36 => "DsReadB32", 0x37 => "DsRead2B32", 0x38 => "DsRead2St64B32", + 0x39 => "DsReadI8", 0x3D => "DsConsume", 0x3E => "DsAppend", 0x4D => "DsWriteB64", @@ -1239,7 +1246,10 @@ public static partial class Gen5ShaderTranslator out uint sizeDwords, out string error) { - var opcode = (word >> 18) & 0x7F; + // GFX10 MIMG uses bit 0 as opcode bit 7. Treating the opcode as only + // bits 18..24 makes the 0x80+ family (including sample-adjust forms) + // decode as the corresponding low opcode. + var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7); name = $"{prefix}Raw{opcode:X2}"; sizeDwords = 2; error = string.Empty; @@ -1287,7 +1297,7 @@ public static partial class Gen5ShaderTranslator out uint sizeDwords, out string error) { - var opcode = (word >> 18) & 0x7F; + var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7); name = opcode switch { 0x00 => "BufferLoadFormatX", @@ -1333,6 +1343,9 @@ public static partial class Gen5ShaderTranslator 0x3B => "BufferAtomicXor", 0x3C => "BufferAtomicInc", 0x3D => "BufferAtomicDec", + 0x3F => "BufferAtomicFmin", + 0x40 => "BufferAtomicFmax", + 0x5A => "BufferAtomicOrX2", _ => $"MubufRaw{opcode:X2}", }; sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u; @@ -1347,7 +1360,7 @@ public static partial class Gen5ShaderTranslator out string error) { var segment = (word >> 14) & 0x3; - var opcode = (word >> 18) & 0x7F; + var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7); sizeDwords = 2; error = string.Empty; var prefix = segment switch @@ -1468,20 +1481,70 @@ public static partial class Gen5ShaderTranslator 0x1B => "ImageAtomicInc", 0x1C => "ImageAtomicDec", 0x20 => "ImageSample", + 0x21 => "ImageSampleCl", 0x22 => "ImageSampleD", + 0x23 => "ImageSampleDCl", 0x24 => "ImageSampleL", 0x25 => "ImageSampleB", + 0x26 => "ImageSampleBCl", 0x27 => "ImageSampleLz", + 0x28 => "ImageSampleC", + 0x29 => "ImageSampleCCl", + 0x2A => "ImageSampleCD", + 0x2B => "ImageSampleCDCl", + 0x2C => "ImageSampleCL", + 0x2D => "ImageSampleCB", + 0x2E => "ImageSampleCBCl", 0x2F => "ImageSampleCLz", 0x30 => "ImageSampleO", + 0x31 => "ImageSampleClO", + 0x32 => "ImageSampleDO", + 0x33 => "ImageSampleDClO", 0x34 => "ImageSampleLO", + 0x35 => "ImageSampleBO", + 0x36 => "ImageSampleBClO", 0x37 => "ImageSampleLzO", + 0x38 => "ImageSampleCO", + 0x39 => "ImageSampleCClO", + 0x3A => "ImageSampleCDO", + 0x3B => "ImageSampleCDClO", + 0x3C => "ImageSampleCLO", + 0x3D => "ImageSampleCBO", + 0x3E => "ImageSampleCBClO", + 0x3F => "ImageSampleCLzO", 0x40 => "ImageGather4", 0x47 => "ImageGather4Lz", 0x48 => "ImageGather4C", - 0x4E => "ImageGather4CBCl", + 0x4F => "ImageGather4CLz", 0x57 => "ImageGather4LzO", + 0x58 => "ImageGather4CO", 0x5F => "ImageGather4CLzO", + 0x60 => "ImageGetLod", + 0x61 => "ImageGather4H", + 0x68 => "ImageSampleCd", + 0x69 => "ImageSampleCdCl", + 0x6A => "ImageSampleCCd", + 0x6B => "ImageSampleCCdCl", + 0x6C => "ImageSampleCdO", + 0x6D => "ImageSampleCdClO", + 0x6E => "ImageSampleCCdO", + 0x6F => "ImageSampleCCdClO", + 0xA0 => "ImageSampleA", + 0xA1 => "ImageSampleClA", + 0xA5 => "ImageSampleBA", + 0xA6 => "ImageSampleBClA", + 0xA8 => "ImageSampleCA", + 0xA9 => "ImageSampleCClA", + 0xAD => "ImageSampleCBA", + 0xAE => "ImageSampleCBClA", + 0xB0 => "ImageSampleAO", + 0xB1 => "ImageSampleClAO", + 0xB5 => "ImageSampleBAO", + 0xB6 => "ImageSampleBClAO", + 0xB8 => "ImageSampleCAO", + 0xB9 => "ImageSampleCClAO", + 0xBD => "ImageSampleCBAO", + 0xBE => "ImageSampleCBClAO", _ => string.Empty, }; @@ -1627,6 +1690,7 @@ public static partial class Gen5ShaderTranslator { "DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or "DsMinI32" or "DsMaxI32" or "DsMinU32" or "DsMaxU32" or + "DsMinF32" or "DsMaxF32" or "DsAndB32" or "DsOrB32" or "DsXorB32" or "DsCmpstB32" or "DsAddRtnU32" or "DsSubRtnU32" or "DsIncRtnU32" or "DsDecRtnU32" or "DsMinRtnI32" or "DsMaxRtnI32" or "DsMinRtnU32" or "DsMaxRtnU32" or @@ -2103,6 +2167,13 @@ public static partial class Gen5ShaderTranslator Gen5Operand.Vector(vectorData0), Gen5Operand.Vector(vectorData1), ], + // GFX10 DS_MIN/MAX_F32 use DATA0 as the replacement value and + // DATA1 as the floating-point compare operand. + "DsMinF32" or "DsMaxF32" => [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorData0), + Gen5Operand.Vector(vectorData1), + ], _ when IsDataShareAtomic(opcode) => [ Gen5Operand.Vector(vectorAddress), Gen5Operand.Vector(vectorData0), @@ -2114,7 +2185,7 @@ public static partial class Gen5ShaderTranslator "DsAppend" or "DsConsume" => [ Gen5Operand.Vector(vectorDestination), ], - "DsReadB32" or "DsReadAddtidB32" or "DsSwizzleB32" => [ + "DsReadB32" or "DsReadI8" or "DsReadAddtidB32" or "DsSwizzleB32" => [ Gen5Operand.Vector(vectorDestination), ], "DsReadB64" or "DsRead2B32" or "DsRead2St64B32" => [ @@ -2272,7 +2343,7 @@ public static partial class Gen5ShaderTranslator "BufferStoreDwordx2" => 2u, "BufferStoreDwordx3" => 3u, "BufferStoreDwordx4" => 4u, - "BufferAtomicCmpswap" => 2u, + "BufferAtomicCmpswap" or "BufferAtomicOrX2" => 2u, _ when opcode.StartsWith("BufferAtomic", StringComparison.Ordinal) => 1u, _ => 0u, }; diff --git a/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs b/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs index 4a79119..a7df5d6 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs @@ -7,7 +7,7 @@ using SharpEmu.ShaderCompiler.Ir; namespace SharpEmu.ShaderCompiler.Resources; -// The fixed binding numbers of one stage: buffers, thirty-six image classes, samplers, +// The fixed binding numbers of one stage: buffers, image classes, samplers, // and the shared buffers. A pixel stage adds Count to every number. public enum DescriptorBindingKind : uint { @@ -44,9 +44,58 @@ public static class ImageDescriptorBinding private const uint StorageFloatBinding = 22; private const uint StorageUintBinding = 27; private const uint AtomicUintBinding = 32; + private const uint SampledCubeFloatBinding = 37; + private const uint SampledCubeUintBinding = 38; + private const uint SampledCubeSintBinding = 39; + private const uint StorageCubeFloatBinding = 40; + private const uint StorageCubeUintBinding = 41; + private const uint AtomicCubeUintBinding = 42; public static DescriptorBindingKind? ForImage(ImageResource image) { + if (image.Cube) + { + if (image.Dimension is not (ImageDimension.Dim2D or ImageDimension.Dim2DArray)) + { + return null; + } + + if (image.ResourceClass == ImageResourceClass.Sampled) + { + if (image.Atomic) + { + return null; + } + + return image.NumericClass switch + { + ImageNumericClass.Float => (DescriptorBindingKind)SampledCubeFloatBinding, + ImageNumericClass.Uint => (DescriptorBindingKind)SampledCubeUintBinding, + ImageNumericClass.Sint => (DescriptorBindingKind)SampledCubeSintBinding, + _ => null, + }; + } + + if (image.ResourceClass == ImageResourceClass.Storage) + { + if (image.Atomic) + { + return image.NumericClass == ImageNumericClass.Uint + ? (DescriptorBindingKind)AtomicCubeUintBinding + : null; + } + + return image.NumericClass switch + { + ImageNumericClass.Float => (DescriptorBindingKind)StorageCubeFloatBinding, + ImageNumericClass.Uint => (DescriptorBindingKind)StorageCubeUintBinding, + _ => null, + }; + } + + return null; + } + uint baseBinding; var sampled = false; if (image.ResourceClass == ImageResourceClass.Sampled) @@ -147,6 +196,16 @@ public static class ImageDescriptorBinding public static ImageResourceClass ResourceClass(DescriptorBindingKind kind) { var value = (uint)kind; + if (value is >= SampledCubeFloatBinding and <= SampledCubeSintBinding) + { + return ImageResourceClass.Sampled; + } + + if (value is >= StorageCubeFloatBinding and <= AtomicCubeUintBinding) + { + return ImageResourceClass.Storage; + } + if (value >= BindingLayout.FirstImageBinding && value < BindingLayout.FirstStorageImageBinding) { return ImageResourceClass.Sampled; @@ -162,6 +221,9 @@ public static class ImageDescriptorBinding public static uint ArrayIndex(DescriptorBindingKind kind) => (uint)kind - BindingLayout.FirstImageBinding; + public static bool IsCube(DescriptorBindingKind kind) => + (uint)kind is >= SampledCubeFloatBinding and <= AtomicCubeUintBinding; + private static readonly ImageDimension[] SampledDimensions = [ ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray, @@ -177,6 +239,29 @@ public static class ImageDescriptorBinding public static (ImageResourceClass ResourceClass, ImageNumericClass NumericClass, ImageDimension Dimension, bool Atomic) Describe(DescriptorBindingKind kind) { var index = (uint)kind; + if (index is >= SampledCubeFloatBinding and <= SampledCubeSintBinding) + { + var numericClass = index switch + { + SampledCubeFloatBinding => ImageNumericClass.Float, + SampledCubeUintBinding => ImageNumericClass.Uint, + _ => ImageNumericClass.Sint, + }; + return (ImageResourceClass.Sampled, numericClass, ImageDimension.Dim2DArray, false); + } + + if (index is >= StorageCubeFloatBinding and <= StorageCubeUintBinding) + { + return (ImageResourceClass.Storage, + index == StorageCubeFloatBinding ? ImageNumericClass.Float : ImageNumericClass.Uint, + ImageDimension.Dim2DArray, false); + } + + if (index == AtomicCubeUintBinding) + { + return (ImageResourceClass.Storage, ImageNumericClass.Uint, ImageDimension.Dim2DArray, true); + } + if (index >= SampledFloatBinding && index < StorageFloatBinding) { var offset = index - SampledFloatBinding; @@ -205,7 +290,7 @@ public sealed class BindingLayout : IEquatable { public const uint FirstImageBinding = 1; public const uint FirstStorageImageBinding = 22; - public const uint ImageBindingCount = 36; + public const uint ImageBindingCount = 42; public const uint NoShaderBase = uint.MaxValue; public const uint ShaderBaseDwordCount = 2; private const int ScalarRegisterCount = 256; diff --git a/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs b/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs index e76b75f..2ddec13 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs @@ -260,9 +260,16 @@ public sealed class MemoryAccessTable var atomic = opcode.StartsWith("ImageAtomic", StringComparison.Ordinal); var store = opcode.StartsWith("ImageStore", StringComparison.Ordinal); var sampled = opcode.StartsWith("ImageSample", StringComparison.Ordinal) || - opcode.StartsWith("ImageGather", StringComparison.Ordinal); + opcode.StartsWith("ImageGather", StringComparison.Ordinal) || + opcode == "ImageGetLod"; var compare = (opcode.StartsWith("ImageSampleC", StringComparison.Ordinal) && !opcode.StartsWith("ImageSampleCd", StringComparison.Ordinal)) || opcode.StartsWith("ImageGather4C", StringComparison.Ordinal); + var mimgOpcode = instruction.Words.Count != 0 + ? ((instruction.Words[0] >> 18) & 0x7F) | ((instruction.Words[0] & 1) << 7) + : 0u; + var adjust = mimgOpcode is + 0xA0 or 0xA1 or 0xA5 or 0xA6 or 0xA8 or 0xA9 or 0xAD or 0xAE or + 0xB0 or 0xB1 or 0xB5 or 0xB6 or 0xB8 or 0xB9 or 0xBD or 0xBE; return new MemoryAccessInfo { Pc = instruction.Pc, @@ -273,7 +280,9 @@ public sealed class MemoryAccessTable NeedsSampler = sampled, Dmask = control.Dmask, ImageDimension = DecodeImageDimension(control.Dimension), - ImageSampleFlags = compare ? ImageSampleFlags.Compare : ImageSampleFlags.None, + ImageSampleFlags = + (compare ? ImageSampleFlags.Compare : ImageSampleFlags.None) | + (adjust ? ImageSampleFlags.Adjust : ImageSampleFlags.None), ImageHasMip = opcode is "ImageLoadMip" or "ImageStoreMip", ImageR128 = instruction.Words.Count != 0 && ((instruction.Words[0] >> 15) & 1) != 0, Glc = control.Glc, diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs index 28e507b..13736ad 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs @@ -146,7 +146,8 @@ public static class ResourceMaterializer for (var candidateIndex = 0; candidateIndex < descriptors.Count; candidateIndex++) { var descriptor = descriptors[candidateIndex]; - if (NullImageDescriptor(descriptor.Dwords) || !ValidImageDescriptor(descriptor.Dwords, image.R128)) + if (NullImageDescriptor(descriptor.Dwords) || !ValidImageDescriptor(descriptor.Dwords, image.R128) || + !ReservedImageBitsClear(descriptor.Dwords)) descriptor = DescriptorWords.Empty(8); var existing = directTable.Descriptors.FindIndex(candidate => candidate.SameAs(descriptor)); if (existing < 0) @@ -161,12 +162,27 @@ public static class ResourceMaterializer if (directTable.Descriptors.Count > 1) snapshot.IndirectImages.Add(directTable); continue; } + if (indirect.Dense) + { + if (!RuntimeValueEvaluator.EvaluateSources(plan, [indirect.HeapSource], cleanInputs, [], evaluateTable: false, + out var denseSources, out _)) + return false; + if (!MaterializeDenseIndirectImage(indirect, denseSources[0], image, image.R128, inputs, out var denseTable, out failure)) + return false; + snapshot.Images[imageIndex] = denseTable.Descriptors[(int)denseTable.Candidates[0]].Dwords; + if (denseTable.Descriptors.Count > 1) + { + denseTable.Resource = (uint)imageIndex; + snapshot.IndirectImages.Add(denseTable); + } + continue; + } if (!RuntimeValueEvaluator.EvaluateSources(plan, [indirect.MaterialSource, indirect.HeapSource], cleanInputs, [], evaluateTable: false, out var tables, out _)) { return false; } - if (!MaterializeIndirectImage(plan, indirect, tables[0], tables[1], image.R128, inputs, + if (!MaterializeIndirectImage(plan, indirect, tables[0], tables[1], image, inputs, captureSelectorDiagnostic, out var indirectTable, out failure)) { return false; @@ -201,6 +217,21 @@ public static class ResourceMaterializer private static bool NullImageDescriptor(ReadOnlySpan descriptor) => descriptor[0] == 0 && (descriptor[1] & 0xFF) == 0; + private static bool ReservedImageBitsClear(ReadOnlySpan descriptor) + { + ReadOnlySpan reserved = + [ + 0x00000000u, 0x20000000u, 0xf0003000u, 0x00000000u, + 0xe000e000u, 0xf9000000u, 0x00007b00u, 0x00000000u, + ]; + for (var dword = 0; dword < reserved.Length; dword++) + { + if ((descriptor[dword] & reserved[dword]) != 0) + return false; + } + return true; + } + private static bool ValidImageDescriptor(ReadOnlySpan descriptor, bool r128) { var type = GuestImageFormat.ImageTypeOf(descriptor); @@ -215,6 +246,20 @@ public static class ResourceMaterializer return false; } + if (type is GuestImageFormat.ImageTypeCube or + GuestImageFormat.ImageType1DArray or + GuestImageFormat.ImageType2DArray or + GuestImageFormat.ImageType2DMsaaArray) + { + var lastSlice = descriptor[4] & 0x1FFF; + var baseSlice = (descriptor[4] >> 16) & 0x1FFF; + if (baseSlice > lastSlice) + { + return false; + } + + } + if (type is GuestImageFormat.ImageType2DMsaa or GuestImageFormat.ImageType2DMsaaArray) { var baseLevel = (descriptor[3] >> 12) & 0xF; @@ -259,7 +304,7 @@ public static class ResourceMaterializer IndirectImageSelector indirect, DescriptorWords material, DescriptorWords heap, - bool r128, + ImageResource image, ResourceRuntimeInputs inputs, bool captureSelectorDiagnostic, out IndirectImageTable result, @@ -322,15 +367,7 @@ public static class ResourceMaterializer } - var table = new IndirectImageTable - { - Keys = keys, - MaterialDescriptor = material.Dwords, - HeapDescriptor = heap.Dwords, - SelectorStride = indirect.SelectorStride, - SelectorOffset = indirect.SelectorOffset, - SelectorDiagnostic = diagnostic, - }; + var probed = new List(keys.Count); foreach (var key in keys) { var candidate = new uint[8]; @@ -343,34 +380,102 @@ public static class ResourceMaterializer } } - if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128)) + if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, image.R128) || + !ReservedImageBitsClear(candidate)) { Array.Clear(candidate); } - - var words = new DescriptorWords(candidate); - var found = table.Descriptors.FindIndex(existing => existing.SameAs(words)); - if (found < 0) - { - if (table.Descriptors.Count >= ShaderResourceInfo.MaxImages) - { - failure = ResourceMaterializationFailure.ImageCapacityExceeded; - return Fail("indirect image candidates exceed the dense image resource limit"); - } - - table.Descriptors.Add(words); - table.Candidates.Add((uint)(table.Descriptors.Count - 1)); - } - else - { - table.Candidates.Add((uint)found); - } + probed.Add(candidate); } + if (!FinishIndirectImage( + probed, + keys, + out var table, + out failure)) + { + return false; + } + + table.MaterialDescriptor = material.Dwords; + table.HeapDescriptor = heap.Dwords; + table.SelectorStride = indirect.SelectorStride; + table.SelectorOffset = indirect.SelectorOffset; + table.SelectorDiagnostic = diagnostic; result = table; return true; } + private static bool MaterializeDenseIndirectImage( + IndirectImageSelector indirect, + DescriptorWords heap, + ImageResource image, + bool r128, + ResourceRuntimeInputs inputs, + out IndirectImageTable result, + out ResourceMaterializationFailure failure) + { + failure = ResourceMaterializationFailure.Other; + result = new IndirectImageTable(); + if (heap.DwordCount != 2 || indirect.KeyBound == 0 || indirect.KeyBound > MaxIndirectImageProbes || inputs.ReadCleanMemory is null) + return false; + + var baseAddress = (((ulong)heap.Dwords[1] << 32) | heap.Dwords[0]) & AddressMask; + var probed = new List((int)indirect.KeyBound); + for (uint key = 0; key < indirect.KeyBound; key++) + { + var candidate = new uint[8]; + var entry = (ulong)indirect.TableOffset + ((ulong)key << 5); + for (uint dword = 0; dword < 8; dword++) + { + var relative = entry + dword * sizeof(uint); + if (relative > AddressMask || baseAddress > AddressMask - relative || + !inputs.ReadCleanMemory(baseAddress + relative, out candidate[dword])) + return false; + } + + if (NullImageDescriptor(candidate) || !ValidImageDescriptor(candidate, r128) || + !ReservedImageBitsClear(candidate)) + Array.Clear(candidate); + probed.Add(candidate); + } + + return FinishIndirectImage( + probed, + Enumerable.Range(0, (int)indirect.KeyBound) + .Select(index => unchecked(indirect.DynamicOffsetBase + ((uint)index << 5))), + out result, + out failure); + } + + private static bool FinishIndirectImage( + IReadOnlyList probed, + IEnumerable keys, + out IndirectImageTable result, + out ResourceMaterializationFailure failure) + { + failure = ResourceMaterializationFailure.Other; + result = new IndirectImageTable(); + result.Keys.AddRange(keys); + foreach (var candidate in probed) + { + var words = new DescriptorWords(candidate); + var found = result.Descriptors.FindIndex(existing => existing.SameAs(words)); + if (found < 0) + { + if (result.Descriptors.Count >= ShaderResourceInfo.MaxImages) + { + failure = ResourceMaterializationFailure.ImageCapacityExceeded; + return false; + } + found = result.Descriptors.Count; + result.Descriptors.Add(words); + } + result.Candidates.Add((uint)found); + } + return true; + } + // ---- specialization ---- private static ImageDimension DescriptorDimension(ReadOnlySpan descriptor, ImageDimension requested) @@ -643,6 +748,7 @@ public static class ResourceMaterializer } var imageClass = images[(int)exemplar]; + var separateSampledDimensions = info.Images[rootIndex].ResourceClass == ImageResourceClass.Sampled; for (var candidate = 0; candidate < images.Count; candidate++) { var image = images[candidate]; @@ -665,12 +771,12 @@ public static class ResourceMaterializer images[candidate] = image; } - // Sampled 2D cases use their own binding and coordinate width. - var separateDimensions = info.Images[rootIndex].ResourceClass == ImageResourceClass.Sampled && - !image.Cube && !imageClass.Cube && + // Each sampled candidate has its own binding and coordinate width. + var compatibleDimensions = image.Dimension == imageClass.Dimension || + separateSampledDimensions && !image.Cube && !imageClass.Cube && image.Dimension is ImageDimension.Dim2D or ImageDimension.Dim2DArray && imageClass.Dimension is ImageDimension.Dim2D or ImageDimension.Dim2DArray; - if (image.NumericClass != imageClass.NumericClass || (!separateDimensions && image.Dimension != imageClass.Dimension) || + if (image.NumericClass != imageClass.NumericClass || !compatibleDimensions || image.MipCount != imageClass.MipCount || image.ConversionFormat != imageClass.ConversionFormat || image.ShaderSwizzle != imageClass.ShaderSwizzle || image.Cube != imageClass.Cube) { @@ -863,8 +969,52 @@ public static class ResourceMaterializer { pair.Sampler = samplerPlan.PointSampler[pair.Sampler]; } + } - info.Samplers[(int)pair.Sampler].DepthCompare |= image.DepthCompare; + // A guest sampler can be shared by ordinary sampling and depth-reference + // sampling. Vulkan bakes compareEnable into VkSampler, so those uses cannot + // share one host sampler. Split only the mixed cases; compare-only samplers + // can use their existing slot. + var compareUsage = new byte[ShaderResourceInfo.MaxSamplers]; + foreach (var pair in info.SampledPairs) + { + var image = info.Images[(int)pair.Image]; + compareUsage[pair.Sampler] |= image.DepthCompare ? (byte)2 : (byte)1; + } + + var compareSampler = new uint[ShaderResourceInfo.MaxSamplers]; + Array.Fill(compareSampler, DescriptorConstants.NoIndex); + for (var index = 0; index < info.Samplers.Count; index++) + { + if ((compareUsage[index] & 2) == 0) + { + continue; + } + + if ((compareUsage[index] & 1) == 0) + { + info.Samplers[index].DepthCompare = true; + compareSampler[index] = (uint)index; + continue; + } + + if (info.Samplers.Count >= ShaderResourceInfo.MaxSamplers) + { + throw new ResourcePlanException($"shader resource specialization exceeds the sampler limit: hash=0x{plan.Hash:X16}"); + } + + var sampler = info.Samplers[index].Clone(); + sampler.DepthCompare = true; + compareSampler[index] = (uint)info.Samplers.Count; + info.Samplers.Add(sampler); + } + + foreach (var pair in info.SampledPairs) + { + if (info.Images[(int)pair.Image].DepthCompare) + { + pair.Sampler = compareSampler[pair.Sampler]; + } } var samplerByMemory = new Dictionary(); @@ -877,9 +1027,25 @@ public static class ResourceMaterializer } var image = info.Images[(int)memory.Resource]; - if (memory.NeedsSampler && RequiresPointSampler(image.NumericClass, image.ConversionFormat) && memory.Sampler < source.Samplers.Count) + if (!memory.NeedsSampler || memory.Sampler >= source.Samplers.Count) { - samplerByMemory[index] = samplerPlan.PointSampler[memory.Sampler]; + continue; + } + + var sampler = memory.Sampler; + if (RequiresPointSampler(image.NumericClass, image.ConversionFormat)) + { + sampler = samplerPlan.PointSampler[sampler]; + } + + if (image.DepthCompare) + { + sampler = compareSampler[sampler]; + } + + if (sampler != memory.Sampler) + { + samplerByMemory[index] = sampler; } } diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs new file mode 100644 index 0000000..64f0dd9 --- /dev/null +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DenseImages.cs @@ -0,0 +1,177 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.ShaderCompiler.Resources; + +public sealed partial class ResourceTracker +{ + private const uint DenseIndirectImageShift = 5; + private const uint MaxDenseIndirectImageEntries = 4096; + + private bool TryMakeDenseIndirectImage(ScalarValue handle, uint pc, out IndirectImagePlan plan) + { + plan = null!; + if (handle.Kind != ScalarValueKind.ImageHandle || handle.Operands.Length != 8) + return false; + + var reads = new ScalarValue[8]; + var memoryIndices = new int[8]; + ScalarValue? heapHandle = null; + ScalarValue? based = null; + uint tableImmediate = 0; + var canSuppressMemoryReads = true; + + for (var dword = 0; dword < 8; dword++) + { + var read = handle.Operands[dword]; + if (read.Kind != ScalarValueKind.ScalarAddressWord || read.MemoryIndex < 0 || + read.MemoryIndex >= _plan.Memory.Count || !MemoryIndexBelongsTo(read.MemoryIndex, read)) + return false; + + var memory = _plan.Memory[read.MemoryIndex]; + if (memory.Kind != MemoryResourceKind.ScalarAddress || memory.DataBits != 32 || memory.DataDwords != 1) + return false; + + var offset = read.Operands[1]; + uint extra = 0; + if (based is null) + { + based = offset; + } + else if (!_graph.Equivalent(offset, based)) + { + if (offset.Kind != ScalarValueKind.Operation || offset.Operation != ScalarOperation.IAdd32 || offset.Operands.Length != 2) + return false; + + ScalarValue inner; + if (offset.Operands[1].IsConstant) + { + extra = offset.Operands[1].ConstantU32; + inner = offset.Operands[0]; + } + else if (offset.Operands[0].IsConstant) + { + extra = offset.Operands[0].ConstantU32; + inner = offset.Operands[1]; + } + else + { + return false; + } + + if (!_graph.Equivalent(inner, based)) + return false; + } + + var componentOffset = checked((uint)dword * sizeof(uint)); + if ((ulong)extra + memory.Offset < componentOffset) + return false; + var immediate = extra + memory.Offset - componentOffset; + if (dword == 0) + tableImmediate = immediate; + else if (immediate != tableImmediate) + return false; + + var currentHandle = read.Operands[0]; + if (currentHandle.Kind != ScalarValueKind.AddressHandle || + (heapHandle is not null && !_graph.Equivalent(currentHandle, heapHandle))) + return false; + + heapHandle = currentHandle; + reads[dword] = read; + memoryIndices[dword] = read.MemoryIndex; + canSuppressMemoryReads &= HasOnlyImageConsumers(memory, handle); + } + + if (based is null || heapHandle is null) + return false; + + uint tableOffset = tableImmediate; + var scaled = based; + if (scaled.Kind == ScalarValueKind.Operation && scaled.Operation == ScalarOperation.IAdd32 && scaled.Operands.Length == 2) + { + if (scaled.Operands[1].IsConstant) + { + tableOffset = unchecked(tableOffset + scaled.Operands[1].ConstantU32); + scaled = scaled.Operands[0]; + } + else if (scaled.Operands[0].IsConstant) + { + tableOffset = unchecked(tableOffset + scaled.Operands[0].ConstantU32); + scaled = scaled.Operands[1]; + } + else + { + return false; + } + } + + if (scaled.Kind != ScalarValueKind.Operation || scaled.Operation != ScalarOperation.ShiftLeft32 || + scaled.Operands.Length != 2 || !scaled.Operands[1].IsConstant || + scaled.Operands[1].ConstantU32 != DenseIndirectImageShift) + return false; + + var key = scaled.Operands[0]; + var bound = DenseKeyBound(key); + if (bound == 0) + return false; + + foreach (var read in reads) + if (!UsesOnly(read, [handle])) + return false; + + if (!MakeRuntimeAddressSource(heapHandle, pc, out var heapSourceIndex, out var heapSource)) + return false; + + var imageDwords = Enumerable.Repeat(key, 8).ToArray(); + imageDwords[0] = heapSource.Dwords[0]; + imageDwords[1] = heapSource.Dwords[1]; + var imageSource = new DescriptorSource + { + Dwords = imageDwords, + IndirectImage = new IndirectImageSelector(0, heapSourceIndex, 0, 0, 0) + { + Dense = true, + TableOffset = tableOffset, + DynamicOffsetBase = unchecked(tableOffset - tableImmediate), + KeyBound = bound, + }, + }; + + plan = new IndirectImagePlan + { + Handle = handle, + Source = InternSource(imageSource), + Key = reads[0], + KeyIsAddressOffset = true, + HeapSource = heapSourceIndex, + SuppressMemoryReads = canSuppressMemoryReads, + Memory = memoryIndices, + Reads = reads, + }; + return true; + } + + private uint DenseKeyBound(ScalarValue key) + { + if (key.Kind == ScalarValueKind.Operation && key.Operation == ScalarOperation.FindLowestBit32 && + _graph.HasNonZeroBitScanInput(key)) + return 32; + + if (!_uses.TryGetValue(key, out var uses)) + return 0; + + foreach (var use in uses) + { + if (use.Kind != ScalarValueKind.Operation || use.Operation != ScalarOperation.ULessThan32 || use.Operands.Length != 2 || + !ReferenceEquals(use.Operands[0], key) || !use.Operands[1].IsConstant) + continue; + + var limit = use.Operands[1].ConstantU32; + if (limit is > 0 and <= MaxDenseIndirectImageEntries) + return limit; + } + + return 0; + } +} diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DirectImages.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DirectImages.cs index 633b4c6..c2da270 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DirectImages.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.DirectImages.cs @@ -13,6 +13,7 @@ public sealed partial class ResourceTracker var reads = handle.Operands; var memoryIndices = new int[8]; + var canSuppressMemoryReads = true; for (var component = 0; component < reads.Length; component++) { var read = reads[component]; @@ -22,7 +23,7 @@ public sealed partial class ResourceTracker var memory = _plan.Memory[read.MemoryIndex]; if (memory.Kind != MemoryResourceKind.ScalarAddress || memory.DataBits != 32 || memory.DataDwords != 1) return false; - if (!HasOnlyImageConsumers(memory, handle)) return false; + canSuppressMemoryReads &= HasOnlyImageConsumers(memory, handle); memoryIndices[component] = read.MemoryIndex; } @@ -86,6 +87,7 @@ public sealed partial class ResourceTracker Source = InternSource(imageSource), Key = keyRead, KeyIsAddressOffset = true, + SuppressMemoryReads = canSuppressMemoryReads, Memory = memoryIndices, Reads = reads, }; diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs index 1f1581f..894ea6e 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs @@ -29,6 +29,7 @@ public sealed partial class ResourceTracker public ScalarValue Key = null!; public uint HeapSource; public bool KeyIsAddressOffset; + public bool SuppressMemoryReads = true; public int[] Memory = new int[8]; public ScalarValue[] Reads = new ScalarValue[8]; } @@ -108,14 +109,17 @@ public sealed partial class ResourceTracker var indirectAccesses = new List(); foreach (var plan in _indirectImages) { - foreach (var index in plan.Memory) + if (plan.SuppressMemoryReads) { - _plan.Memory[index].PlanningOnly = true; - } + foreach (var index in plan.Memory) + { + _plan.Memory[index].PlanningOnly = true; + } - foreach (var read in plan.Reads) - { - indirectReads.Add(read); + foreach (var read in plan.Reads) + { + indirectReads.Add(read); + } } for (var index = 0; index < _plan.Accesses.Length; index++) @@ -578,7 +582,7 @@ public sealed partial class ResourceTracker // ---- indirect images ---- private bool IsIndirectPlanningMemory(int index) => - _indirectImages.Any(plan => plan.Memory.Contains(index)); + _indirectImages.Any(plan => plan.SuppressMemoryReads && plan.Memory.Contains(index)); private void PlanIndirectImages() { @@ -591,7 +595,9 @@ public sealed partial class ResourceTracker continue; } - if (TryMakeIndirectImage(handle, memory.Pc, out var plan) || TryMakeDirectImage(handle, out plan)) + if (TryMakeIndirectImage(handle, memory.Pc, out var plan) || + TryMakeDenseIndirectImage(handle, memory.Pc, out plan) || + TryMakeDirectImage(handle, out plan)) { _indirectImages.Add(plan); } @@ -635,6 +641,25 @@ public sealed partial class ResourceTracker return true; } + private bool MakeRuntimeAddressSource(ScalarValue handle, uint pc, out uint sourceIndex, out DescriptorSource source) + { + sourceIndex = 0; + source = null!; + if (handle.Kind != ScalarValueKind.AddressHandle) + { + return false; + } + + source = MakeSource(handle, 2, false, false, pc); + if (!ValidateSource(source, out _)) + { + return false; + } + + sourceIndex = InternSource(source); + return true; + } + private static bool MatchMaterialOffset(ScalarValue value, out ScalarValue selector, out uint stride, out uint offset) { selector = null!; diff --git a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs index 01257c2..69a814f 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs @@ -142,7 +142,7 @@ public sealed record StageOutput(StageOutputKind Kind, uint Index, uint Location public sealed class ShaderResourceInfo { public const int MaxBuffers = 32; - public const int MaxImages = 32; + public const int MaxImages = 64; public const int MaxSamplers = 32; public const int MaxSampledPairs = 64; public const int NoScalarRegister = -1; @@ -185,6 +185,10 @@ public sealed record IndirectImageSelector( { public IndirectSelectorValues? SelectorValues { get; init; } public IReadOnlyList? DirectCandidates { get; init; } + public bool Dense { get; init; } + public uint TableOffset { get; init; } + public uint DynamicOffsetBase { get; init; } + public uint KeyBound { get; init; } } public sealed record DirectImageCandidate(uint Offset, uint Source); diff --git a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs index 2c79150..c625208 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs @@ -126,6 +126,10 @@ public sealed class ShaderResourcePlan foreach (var candidate in directCandidates) plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)candidate.Source], cleanSlots); } + else if (indirect.Dense) + { + plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)indirect.HeapSource], cleanSlots); + } else { plan.MarkCleanFlatSlots(plan.DescriptorSources[(int)indirect.MaterialSource], cleanSlots); diff --git a/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs b/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs index 8d49204..3cd0391 100644 --- a/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs +++ b/src/SharpEmu.ShaderCompiler/ShaderCompileRequest.cs @@ -11,10 +11,48 @@ namespace SharpEmu.ShaderCompiler; public sealed record ShaderVertexInput( uint Pc, uint Location, + uint FetchComponentCount, uint ComponentCount, uint NumberFormat, + uint DestinationSelect, bool PerInstance, - IReadOnlyList AliasPcs); + IReadOnlyList AliasPcs) +{ + public ShaderVertexInput( + uint pc, + uint location, + uint componentCount, + uint numberFormat, + bool perInstance, + IReadOnlyList aliasPcs) + : this( + pc, + location, + componentCount, + componentCount, + numberFormat, + componentCount switch + { + 1u => 4u, + 2u => 4u | (5u << 3), + 3u => 4u | (5u << 3) | (6u << 6), + 4u => 4u | (5u << 3) | (6u << 6) | (7u << 9), + _ => 0u, + }, + perInstance, + aliasPcs) + { + } +} + +public readonly record struct ShaderClipSpaceTransform( + bool Enabled, + float ScaleX, + float ScaleY, + float OffsetX, + float OffsetY, + float HalfExtentX, + float HalfExtentY); // Everything an emitter needs to compile one permutation of a program: the decoded // program, its resource plan applied to one specialization, and the binding layout. @@ -99,11 +137,14 @@ public sealed class ShaderCompileRequest public IReadOnlyList PixelOutputs { get; init; } = []; public uint PixelInputEnable { get; init; } + public uint PixelCustomInterpolationMask { get; init; } public uint PixelInputAddress { get; init; } public IReadOnlyList? PixelInputCntl { get; init; } public int RequiredVertexOutputCount { get; init; } public IReadOnlyList VertexInputs { get; init; } = []; + public uint PositionExportControl { get; init; } + public ShaderClipSpaceTransform ClipSpace { get; init; } public uint LocalSizeX { get; init; } = 1; public uint LocalSizeY { get; init; } = 1; diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcContextStateTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcContextStateTests.cs new file mode 100644 index 0000000..1906841 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcContextStateTests.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using SharpEmu.Libs.Gpu.GpuCommands; +using SharpEmu.Libs.Tests.Gpu.GpuCommands; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +[Collection(AgcCommandBufferChainCollection.Name)] +public sealed class AgcContextStateTests +{ + private const ulong MemoryBase = 0x210000000; + private const ulong CommandBuffer = MemoryBase + 0x40; + private const ulong Commands = MemoryBase + 0x200; + + private static uint[] Emit(uint operation, uint capacity, out ulong result, out ulong cursor) + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + Assert.True(context.TryWriteUInt64(CommandBuffer + 0x10, Commands)); + Assert.True(context.TryWriteUInt64(CommandBuffer + 0x18, Commands + capacity * 4)); + context[CpuRegister.Rdi] = CommandBuffer; + context[CpuRegister.Rsi] = operation; + AgcExports.DcbContextStateOperation(context); + result = context[CpuRegister.Rax]; + Span cursorBytes = stackalloc byte[8]; + Assert.True(memory.TryRead(CommandBuffer + 0x10, cursorBytes)); + cursor = BinaryPrimitives.ReadUInt64LittleEndian(cursorBytes); + var bytes = new byte[(int)(cursor - Commands)]; + Assert.True(memory.TryRead(Commands, bytes)); + return Enumerable.Range(0, bytes.Length / 4) + .Select(index => BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * 4))).ToArray(); + } + + [Theory] + [InlineData(0u, 5u)] + [InlineData(1u, 27u)] + [InlineData(2u, 27u)] + [InlineData(3u, 32u)] + public void ExportPreservesPacketSizesAndOperation(uint operation, uint expectedDwords) + { + var words = Emit(operation, expectedDwords, out var result, out var cursor); + Assert.Equal(Commands, result); + Assert.Equal(Commands + expectedDwords * 4, cursor); + Assert.Equal(operation, words[1]); + for (var index = 0; index < words.Length;) + { + Assert.Equal(PacketOpcode.Nop, PacketHeader.Opcode(words[index])); + Assert.Equal(index == 0 ? PacketCustomCode.ContextState : 0u, PacketHeader.CustomCode(words[index])); + index += (int)PacketHeader.Length(words[index]); + Assert.InRange(index, 1, words.Length); + } + } + + [Fact] + public void ExportedPushClearAndPopRestoreViewportAndTargetMask() + { + var push = Emit(3, 32, out _, out _); + var pop = Emit(2, 27, out _, out _); + var runner = new StreamRunner(); + runner.Run( + StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x113, 0x3F800000u, 0u), + StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x8E, 0xFu), + push, + StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x113, 0x3F000000u, 0x3F000000u), + StreamRunner.Packet(PacketOpcode.SetContextRegister, 0x8E, 0u)); + Assert.Equal(0.5f, runner.Interpreter.TypedRegisters.Context.ScreenViewport.Viewports[0].ZOffset); + runner.Run(pop); + var restored = runner.Interpreter.TypedRegisters.Context; + Assert.Equal(1f, restored.ScreenViewport.Viewports[0].ZScale); + Assert.Equal(0f, restored.ScreenViewport.Viewports[0].ZOffset); + Assert.Equal(0xFu, restored.RenderTargetMask); + Assert.False(runner.Interpreter.TypedRegisters.ContextPushed); + } + + [Theory] + [InlineData(4u, 32u)] + [InlineData(1u, 21u)] + [InlineData(0u, 4u)] + public void InvalidOperationOrInsufficientSpaceReturnsNull(uint operation, uint capacity) + { + Assert.Empty(Emit(operation, capacity, out var result, out var cursor)); + Assert.Equal(0UL, result); + Assert.Equal(Commands, cursor); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcResourceOwnerTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcResourceOwnerTests.cs index aac94fc..6be9316 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcResourceOwnerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcResourceOwnerTests.cs @@ -58,4 +58,34 @@ public sealed class AgcResourceOwnerTests Assert.True(memory.TryRead(address, buffer)); return BinaryPrimitives.ReadUInt32LittleEndian(buffer); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UnregisterOwnerUsesTheSharedRegistryAcrossMemoryWrappers(bool nested) + { + var memory = new FakeCpuMemory(BaseAddress, 0x2000); + memory.WriteCString(NameAddress, "Owner"); + var registration = new CpuContext(new MemoryWrapper(memory), Generation.Gen5); + registration[CpuRegister.Rdi] = OwnerAddress; + registration[CpuRegister.Rsi] = NameAddress; + Assert.Equal(0, AgcExports.DriverRegisterOwner(registration)); + var owner = ReadUInt32(memory, OwnerAddress); + ICpuMemory removalMemory = new MemoryWrapper(memory); + if (nested) + removalMemory = new MemoryWrapper(removalMemory); + var removal = new CpuContext(removalMemory, Generation.Gen5); + removal[CpuRegister.Rdi] = owner; + Assert.Equal(0, AgcExports.DriverUnregisterOwnerAndResources(removal)); + registration[CpuRegister.Rdi] = owner; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, + AgcExports.DriverUnregisterOwnerAndResources(registration)); + } + + private sealed class MemoryWrapper(ICpuMemory inner) : ICpuMemory, ICpuMemoryWrapper + { + public ICpuMemory Inner => inner; + public bool TryRead(ulong address, Span destination) => inner.TryRead(address, destination); + public bool TryWrite(ulong address, ReadOnlySpan source) => inner.TryWrite(address, source); + } } diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs index 9bf8a6e..b1928a7 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs @@ -44,6 +44,35 @@ public sealed class Gen5ShaderAtomicDecodeTests Assert.Equal(1u, control.VectorData); } + [Theory] + [InlineData(0xE0FC4000u, "BufferAtomicFmin")] + [InlineData(0xE1004000u, "BufferAtomicFmax")] + public void BufferAtomicFloatMinMax_Decode(uint word, string opcode) + { + var instruction = DecodeSingle(word, 0x80000100); + + Assert.Equal(opcode, instruction.Opcode); + var control = Assert.IsType(instruction.Control); + Assert.Equal(1u, control.DwordCount); + Assert.Equal(1u, control.VectorData); + Assert.Equal(0u, control.ScalarResource); + } + + [Fact] + public void BufferAtomicOrX2_UsesTwoDataRegisters() + { + // BUFFER_ATOMIC_OR_X2 v[1:2], off, s[0:3], 128 glc + var instruction = DecodeSingle(0xE1684000, 0x80000100); + + Assert.Equal("BufferAtomicOrX2", instruction.Opcode); + var control = Assert.IsType(instruction.Control); + Assert.Equal(2u, control.DwordCount); + Assert.Equal(1u, control.VectorData); + Assert.Equal( + new[] { Gen5Operand.Vector(1), Gen5Operand.Vector(2) }, + instruction.Destinations); + } + [Fact] public void ImageAtomicAdd_KeepsDataRegisterAsDestination() { @@ -84,6 +113,21 @@ public sealed class Gen5ShaderAtomicDecodeTests Assert.Equal(new[] { Gen5Operand.Vector(3) }, instruction.Destinations); } + [Theory] + [InlineData(0xD8480004u, "DsMinF32")] + [InlineData(0xD84C0008u, "DsMaxF32")] + public void DsFloatMinMax_KeepReplacementAndCompareOperands(uint word, string opcode) + { + // DATA0 is the replacement value and DATA1 is the float compare operand. + var instruction = DecodeSingle(word, 0x00010907); + + Assert.Equal(opcode, instruction.Opcode); + Assert.Equal( + new[] { Gen5Operand.Vector(7), Gen5Operand.Vector(9), Gen5Operand.Vector(1) }, + instruction.Sources); + Assert.Empty(instruction.Destinations); + } + [Fact] public void DsCmpstRtnB32_OrdersComparatorBeforeNewValue() { @@ -111,6 +155,19 @@ public sealed class Gen5ShaderAtomicDecodeTests Assert.Equal(0x0808u, control.SingleOffsetBytes); } + [Fact] + public void DsReadI8_DecodesAddressAndDestination() + { + // DS_READ_I8 v5, v7 offset:3 + var instruction = DecodeSingle(0xD8E40003, 0x05000007); + + Assert.Equal("DsReadI8", instruction.Opcode); + Assert.Equal(new[] { Gen5Operand.Vector(7) }, instruction.Sources); + Assert.Equal(new[] { Gen5Operand.Vector(5) }, instruction.Destinations); + var control = Assert.IsType(instruction.Control); + Assert.Equal(3u, control.SingleOffsetBytes); + } + [Theory] [InlineData(0xD8FA3412u, "DsAppend")] [InlineData(0xD8F63412u, "DsConsume")] @@ -128,6 +185,20 @@ public sealed class Gen5ShaderAtomicDecodeTests Assert.False(control.Gds); } + [Fact] + public void VCmpxNeU64_DecodesVectorRegisterPairs() + { + // V_CMPX_NE_U64 v[0:1], v[3:4]. The translator consumes each encoded + // source as the low register of a 64-bit pair and updates EXEC. + var instruction = DecodeSingle(0x7DEA0700); + + Assert.Equal("VCmpxNeU64", instruction.Opcode); + Assert.Equal( + new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(3) }, + instruction.Sources); + Assert.Empty(instruction.Destinations); + } + private static Gen5ShaderInstruction DecodeSingle(params uint[] words) { var memory = new FakeCpuMemory(ShaderAddress, 0x1000); diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs index b6ff087..d7851a1 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs @@ -35,6 +35,30 @@ public sealed class Gen5SpirvAtomicTranslationTests Assert.Contains((ushort)SpirvOp.AtomicIIncrement, opcodes); } + [Theory] + [InlineData(0xE0FC4000u, SpirvOp.FOrdLessThan)] + [InlineData(0xE1004000u, SpirvOp.FOrdGreaterThan)] + public void BufferFloatAtomics_EmitCompareExchangeLoop(uint word, SpirvOp compareOp) + { + var opcodes = CompileComputeOpcodes( + [word, 0x80000100], + BufferDescriptorRegisters()); + + Assert.Contains((ushort)SpirvOp.AtomicLoad, opcodes); + Assert.Contains((ushort)SpirvOp.AtomicCompareExchange, opcodes); + Assert.Contains((ushort)compareOp, opcodes); + } + + [Fact] + public void BufferAtomicOrX2_EmitsAtomicOr() + { + var opcodes = CompileComputeOpcodes( + [0xE1684000, 0x80000100], + BufferDescriptorRegisters()); + + Assert.Contains((ushort)SpirvOp.AtomicOr, opcodes); + } + [Fact] public void DataShareAtomics_EmitAtomicOpcodes() { diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AmprCompletionEventTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AmprCompletionEventTests.cs index 327595f..1e54f7c 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AmprCompletionEventTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AmprCompletionEventTests.cs @@ -1,6 +1,9 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Ampr; using SharpEmu.Libs.Kernel; using Xunit; @@ -14,4 +17,75 @@ public sealed class AmprCompletionEventTests Assert.Equal(-25, KernelEventQueueCompatExports.KernelEventFilterAmpr); Assert.Equal(-30, KernelEventQueueCompatExports.KernelEventFilterAmprSystem); } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void CompletionUsesAmprRegistrationAndPreservesPayload(bool completionCommand, bool userRegistrationOnly) + { + const ulong memoryBase = 0x100000000; + const ulong commandBuffer = memoryBase + 0x100; + const ulong eventIdentifier = 29954; + const ulong eventData = 0x10000000000003E8; + const ulong registeredUserData = 0x12345678; + var memory = new FakeCpuMemory(memoryBase, 0x4000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = memoryBase; + Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(context)); + Assert.True(context.TryReadUInt64(memoryBase, out var queue)); + try + { + context[CpuRegister.Rdi] = queue; + context[CpuRegister.Rsi] = eventIdentifier; + Assert.Equal(0, KernelEventQueueCompatExports.KernelAddUserEvent(context)); + if (!userRegistrationOnly) + { + context[CpuRegister.Rdx] = registeredUserData; + Assert.Equal(0, KernelEventQueueCompatExports.KernelAddAmprEvent(context)); + } + + context[CpuRegister.Rdi] = commandBuffer; + context[CpuRegister.Rsi] = memoryBase + 0x200; + context[CpuRegister.Rdx] = 0x100; + Assert.Equal(0, AmprExports.CommandBufferConstructor(context)); + Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context)); + context[CpuRegister.Rsi] = queue; + context[CpuRegister.Rdx] = eventIdentifier; + context[CpuRegister.Rcx] = eventData; + Assert.Equal(0, completionCommand + ? AmprExports.CommandBufferWriteKernelEventQueueOnCompletion(context) + : AmprExports.CommandBufferWriteKernelEventQueue0400(context)); + Assert.Equal(0, AmprExports.CompleteCommandBuffer(context, commandBuffer, out var result, out var errorOffset)); + Assert.Equal(userRegistrationOnly ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND : 0, result); + Assert.Equal(0u, errorOffset); + + context[CpuRegister.Rdi] = queue; + context[CpuRegister.Rsi] = memoryBase + 0x400; + context[CpuRegister.Rdx] = 1; + context[CpuRegister.Rcx] = memoryBase + 0x500; + context[CpuRegister.R8] = memoryBase + 0x600; + var waitResult = KernelEventQueueCompatExports.KernelWaitEqueue(context); + if (userRegistrationOnly) + { + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, waitResult); + return; + } + + Assert.Equal(0, waitResult); + Span eventBytes = stackalloc byte[32]; + Assert.True(memory.TryRead(memoryBase + 0x400, eventBytes)); + Assert.Equal(eventIdentifier, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes)); + Assert.Equal((short)-25, + BinaryPrimitives.ReadInt16LittleEndian(eventBytes[8..])); + Assert.Equal(eventData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes[16..])); + Assert.Equal(registeredUserData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes[24..])); + } + finally + { + context[CpuRegister.Rdi] = queue; + Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(context)); + } + } } diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs index 9e818aa..3a1fa1b 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs @@ -11,6 +11,34 @@ namespace SharpEmu.Libs.Tests.Ampr; [Collection("AmprFileRegistry")] public class AmprFileRegistryTests { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ResolveCollidingPaths_PreservesBothHandles(bool reverseOrder) + { + AmprFileRegistry.ClearForTests(); + const string firstPath = "$/assets/bce5a816.bin"; + const string secondPath = "$/assets/1b9e7058.bin"; + Assert.Equal(AmprFileRegistry.ComputeFileId(firstPath), AmprFileRegistry.ComputeFileId(secondPath)); + var firstHost = Path.Combine(Path.GetTempPath(), "apr-first.bin"); + var secondHost = Path.Combine(Path.GetTempPath(), "apr-second.bin"); + if (reverseOrder) + AmprFileRegistry.Register(secondPath, secondHost); + var firstId = AmprFileRegistry.Register(firstPath, firstHost); + var secondId = AmprFileRegistry.Register(secondPath, secondHost); + Assert.NotEqual(firstId, secondId); + Assert.NotEqual(uint.MaxValue, firstId); + Assert.True(AmprFileRegistry.TryGetHostPath(firstId, out var firstResult)); + Assert.True(AmprFileRegistry.TryGetHostPath(secondId, out var secondResult)); + Assert.Equal(firstHost, firstResult); + Assert.Equal(secondHost, secondResult); + Assert.Equal(firstId, AmprFileRegistry.Register(firstPath, firstHost)); + Assert.False(AmprFileRegistry.TryGetHostPath(AmprFileRegistry.ComputeFileId(firstPath), out _)); + AmprFileRegistry.RegisterApp0RelativeForTests("assets/1b9e7058.bin", secondHost); + Assert.True(AmprFileRegistry.TryGetHostPath(firstId, out firstResult)); + Assert.Equal(firstHost, firstResult); + } + [Fact] public void ComputeFileId_matches_utf8_fnv1a() { @@ -149,6 +177,6 @@ public class AmprFileRegistryTests hash *= prime; } - return hash; + return hash & 0x7fffffff; } } diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AmprWriteAddressTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AmprWriteAddressTests.cs index de652c3..7512ef0 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AmprWriteAddressTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AmprWriteAddressTests.cs @@ -49,6 +49,7 @@ public sealed class AmprWriteAddressTests context[CpuRegister.Rdx] = 0x100; Assert.Equal(0, AmprExports.CommandBufferConstructor(context)); + Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context)); context[CpuRegister.Rdi] = commandBufferAddress; context[CpuRegister.Rsi] = watcherAddress; diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs index 826dd67..cbc87d5 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AprStreamingContractTests.cs @@ -12,6 +12,8 @@ namespace SharpEmu.Libs.Tests.Ampr; [Collection("AmprFileRegistry")] public sealed class AprStreamingContractTests { + public AprStreamingContractTests() => AmprFileRegistry.ClearForTests(); + [Fact] public void ResolveStatAndReadFile_UsesSharedAprFileId() { @@ -71,6 +73,7 @@ public sealed class AprStreamingContractTests context[CpuRegister.Rdx] = 0x100; Assert.Equal(0, AmprExports.CommandBufferConstructor(context)); + Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context)); const ulong readOffset = 2; const ulong readSize = 4; @@ -85,16 +88,10 @@ public sealed class AprStreamingContractTests Span destination = stackalloc byte[(int)readSize]; Assert.True(memory.TryRead(destinationAddress, destination)); + Assert.Equal(new byte[(int)readSize], destination.ToArray()); + Assert.Equal(0, AmprExports.CompleteCommandBuffer(context, commandBufferAddress)); + Assert.True(memory.TryRead(destinationAddress, destination)); Assert.Equal(fileContents.AsSpan((int)readOffset, (int)readSize), destination); - - Span record = stackalloc byte[0x30]; - Assert.True(memory.TryRead(recordBufferAddress, record)); - Assert.Equal(1U, BinaryPrimitives.ReadUInt32LittleEndian(record)); - Assert.Equal(fileId, BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..])); - Assert.Equal(destinationAddress, BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..])); - Assert.Equal(readSize, BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..])); - Assert.Equal(readOffset, BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..])); - Assert.Equal(readSize, BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..])); } finally { @@ -106,6 +103,65 @@ public sealed class AprStreamingContractTests } } + [Fact] + public void ReadFile_UsesCapturedNativeImportStackArgument() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong commandBufferAddress = memoryBase + 0x100; + const ulong recordBufferAddress = memoryBase + 0x200; + const ulong destinationAddress = memoryBase + 0x400; + const ulong fileOffset = 0x1234; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + + context[CpuRegister.Rdi] = commandBufferAddress; + Assert.Equal(0, AmprExports.CommandBufferConstructor(context)); + + context[CpuRegister.Rdi] = commandBufferAddress; + context[CpuRegister.Rsi] = recordBufferAddress; + context[CpuRegister.Rdx] = 0x100; + Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context)); + + context[CpuRegister.Rsp] = 0x7FFF_FFFF_F000; + context.SetImportStackArguments(fileOffset, 0, 0, 0, 0, 0); + context[CpuRegister.Rdi] = commandBufferAddress; + context[CpuRegister.Rcx] = 0x8000_0001; + context[CpuRegister.R8] = destinationAddress; + context[CpuRegister.R9] = 0x20; + + Assert.Equal(0, AmprExports.AprCommandBufferReadFile(context)); + Assert.Equal(0x14u, ReadUInt32(memory, commandBufferAddress + 4)); + } + + [Fact] + public void ReadFile_SequentialOffsetSentinel_DoesNotFaultDuringAppend() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong commandBufferAddress = memoryBase + 0x100; + const ulong recordBufferAddress = memoryBase + 0x200; + const ulong destinationAddress = memoryBase + 0x400; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + + context[CpuRegister.Rdi] = commandBufferAddress; + Assert.Equal(0, AmprExports.CommandBufferConstructor(context)); + + context[CpuRegister.Rdi] = commandBufferAddress; + context[CpuRegister.Rsi] = recordBufferAddress; + context[CpuRegister.Rdx] = 0x100; + Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context)); + + context[CpuRegister.Rsp] = 0x7FFF_FFFF_F000; + context.SetImportStackArguments(ulong.MaxValue, 0, 0, 0, 0, 0); + context[CpuRegister.Rdi] = commandBufferAddress; + context[CpuRegister.Rcx] = 0x8000_0001; + context[CpuRegister.R8] = destinationAddress; + context[CpuRegister.R9] = 0x20; + + Assert.Equal(0, AmprExports.AprCommandBufferReadFile(context)); + Assert.Equal(0x18u, ReadUInt32(memory, commandBufferAddress + 4)); + } + [Fact] public void ResolveFilepathsWithPrefixToIdsAndFileSizes_CombinesPrefixAndResolvesRealFile() { diff --git a/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs index 48e7acd..48d0bfe 100644 --- a/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs +++ b/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs @@ -14,6 +14,7 @@ public sealed class MemcpyHleRoutingTests { private const string MemcpyNid = "Q3VBxCXhUHs"; private const string MemsetNid = "QrZZdJ8XsX0"; + private const string StrcasecmpNid = "AV6ipCNa4Rw"; private const string RdtscNid = "-2IRUCO--PM"; [Fact] @@ -34,6 +35,14 @@ public sealed class MemcpyHleRoutingTests $"memset ({MemsetNid}) must route through HLE on every platform."); } + [Fact] + public void IsHlePreferredNid_PrefersHleForStrcasecmp() + { + Assert.True( + InvokeIsHlePreferredNid(StrcasecmpNid), + $"strcasecmp ({StrcasecmpNid}) must route through HLE so null-argument recovery remains active."); + } + [Fact] public void TryCreateNativeImportIntrinsic_DoesNotClaimMemcpy() { @@ -52,6 +61,22 @@ public sealed class MemcpyHleRoutingTests Assert.Equal(0, address); } + [Fact] + public void TryCreateNativeImportIntrinsic_DoesNotClaimStrcasecmp() + { + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + return; + } + + var claimed = InvokeTryCreateNativeImportIntrinsic(StrcasecmpNid, out var address); + + Assert.False( + claimed, + $"strcasecmp ({StrcasecmpNid}) must fall through to the HLE trampoline so a null guest pointer does not fault in the intrinsic stub."); + Assert.Equal(0, address); + } + [Fact] public void TryCreateNativeImportIntrinsic_StillClaimsNonHleNids() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs index 94f6073..ef00edc 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs @@ -168,7 +168,7 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost Calls.Add($"draw_auto {submitId} {arguments.VertexCount}"); } - public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator) => + public void DispatchDirect(ulong submitId, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator, ulong indirectArgumentsAddress = 0) => Calls.Add($"dispatch {submitId} {groupsX} {groupsY} {groupsZ} {dispatchInitiator:X}"); public void OnQueueReset(int queueId) => Calls.Add($"queue_reset {queueId}"); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs index 7e0cc3f..49b8a3f 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamWorkerTests.cs @@ -67,7 +67,11 @@ public sealed class CommandStreamWorkerTests var (host, queue, worker) = NewWorker(cancelBlockedAtStop: true); host.WriteDword(Label, 0); Enqueue(host, queue, 1, WaitEqual(Label, 1), CreateInstanceCountPacket(6)); - var waiter = Task.Run(queue.WaitForIdle); + var waiter = Task.Factory.StartNew( + queue.WaitForIdle, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); Assert.Equal(IdleOutcome.Cancelled, worker.Stop()); Assert.Equal(IdleOutcome.Cancelled, await waiter.WaitAsync(TimeSpan.FromSeconds(2))); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterMemoryTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterMemoryTests.cs index 7278468..6259e3d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterMemoryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/GpuCommandInterpreterMemoryTests.cs @@ -151,6 +151,48 @@ public sealed class GpuCommandInterpreterMemoryTests Assert.Single(runner.Host.Calls)); } + [Fact] + public void CopyDataBuilder_EncodesGuestArguments() + { + var runner = new StreamRunner(); + var context = new CpuContext(runner.Host.Memory, Generation.Gen5); + const ulong commandBufferAddress = StreamRunner.TableAddress; + const ulong stackAddress = StreamRunner.TableAddress + 0x100; + const ulong immediateValue = 0x1122_3344_5566_7788; + runner.Host.WriteQword(commandBufferAddress + 0x10, StreamRunner.CommandAddress); + runner.Host.WriteQword(commandBufferAddress + 0x18, StreamRunner.CommandAddress + 0x100); + context[CpuRegister.Rdi] = commandBufferAddress; + context[CpuRegister.Rsi] = 2; + context[CpuRegister.Rdx] = 3; + context[CpuRegister.Rcx] = Label; + context[CpuRegister.R8] = 10; + context[CpuRegister.R9] = 2; + context[CpuRegister.Rsp] = stackAddress; + runner.Host.WriteQword(stackAddress + 8, immediateValue); + runner.Host.WriteQword(stackAddress + 16, 1); + runner.Host.WriteQword(stackAddress + 24, 1); + + AgcExports.DcbCopyData(context); + + var expectedControl = + ((10u >> 1) & 0xFu) | + (((2u >> 1) & 0xFu) << 8) | + ((2u & 0x3u) << 13) | + (1u << 16) | + (1u << 20) | + ((3u & 0x3u) << 25) | + ((10u & 0x1u) << 30); + Assert.Equal(StreamRunner.CommandAddress, context[CpuRegister.Rax]); + Assert.Equal(StreamRunner.CommandAddress + 24, runner.Host.ReadQword(commandBufferAddress + 0x10)); + Assert.Equal(PacketHeader.Make(6, PacketOpcode.CopyData), runner.Host.ReadDword(StreamRunner.CommandAddress)); + Assert.Equal(expectedControl, runner.Host.ReadDword(StreamRunner.CommandAddress + 4)); + Assert.Equal(immediateValue, runner.Host.ReadQword(StreamRunner.CommandAddress + 8)); + Assert.Equal(Label, runner.Host.ReadQword(StreamRunner.CommandAddress + 16)); + + runner.Interpreter.Process(new PacketCursorStack(), StreamRunner.CommandAddress, 6); + Assert.Equal(immediateValue, runner.Host.ReadQword(Label)); + } + [Fact] public void CopyData_MapsSelectorsToTransfers() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs index c023ce0..f00fefc 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs @@ -32,6 +32,27 @@ public sealed class ImageRequestBuildersTests : IClassFixture _vulkan = fixture.Vulkan; diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs index 902ffda..096fc5b 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/GraphicsRejectionPolicyTests.cs @@ -65,7 +65,7 @@ public sealed class GraphicsRejectionPolicyTests [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] - public void RejectedPixelCandidatesSkipOnlyWhenStrictModeIsOff(bool strict, bool exceedCapacity) + public void PixelCandidateValidationHonorsCapacityAndStrictMode(bool strict, bool distinctCandidates) { using var fatal = new FatalScope(); var guest = new PipelineTestGuest(); @@ -74,11 +74,19 @@ public sealed class GraphicsRejectionPolicyTests 0xF0000108, 0x00010400, 0xBF810000]); for (var index = 0; index < 33; index++) guest.WriteWords(TableAddress + 312 + (ulong)index * 32, - exceedCapacity ? 0x1000u + (uint)index : 0x1000u, 20u << 20, 0, - 0xFACu | ((!exceedCapacity && index == 1 ? 10u : 9u) << 28), 0, 0, 0, 0); + distinctCandidates ? 0x1000u + (uint)index : 0x1000u, 20u << 20, 0, + 0xFACu | ((!distinctCandidates && index == 1 ? 10u : 9u) << 28), 0, 0, 0, 0); var source = guest.Source(PixelAddress, ShaderStage.Pixel, [(uint)(TableAddress & uint.MaxValue), (uint)(TableAddress >> 32)]); var options = new StageCompileOptions { PixelInfo = new PixelInputInfo() }; var cursor = 7u; + if (distinctCandidates) + { + Assert.True(guest.Programs.TryGetProgram(source, options, strict, ref cursor, out var program, out _, out var rejection), rejection); + Assert.True(program.IsValid); + Assert.Single(guest.Compiler.Requests); + Assert.Single(guest.Host.Modules); + return; + } if (strict) { var failure = Assert.Throws(() => guest.Programs.TryGetProgram( diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs index b6225c9..63b750e 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/VertexInputResolverTests.cs @@ -29,6 +29,9 @@ public sealed class VertexInputResolverTests : IDisposable private const uint BufferFormat32x2Float = 64; private const uint BufferFormat32x4Float = 77; private const uint IdentitySelect = 4 | (5 << 3) | (6 << 6) | (7 << 9); + private const uint X001Select = 4 | (0 << 3) | (0 << 6) | (1 << 9); + private const uint XY01Select = 4 | (5 << 3) | (0 << 6) | (1 << 9); + private const uint XYZ1Select = 4 | (5 << 3) | (6 << 6) | (1 << 9); private readonly FatalScope _fatal = new(); private readonly FakeCpuMemory _memory = new(MemoryBase, 0x1_0000); @@ -202,7 +205,7 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Equal(16u, resource.Descriptor.Stride); Assert.Equal(3u, resource.Descriptor.RecordCount); Assert.Equal(BufferFormat32x2Float, resource.Descriptor.Format); - Assert.Equal(IdentitySelect, resource.Descriptor.DestinationSelectXYZW); + Assert.Equal(XY01Select, resource.Descriptor.DestinationSelectXYZW); Assert.Equal(-1, resource.BufferIndex); } @@ -241,15 +244,20 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Equal(BufferFormat32x2Float, resources[1].Descriptor.Format); } - [Fact] - public void ApplySemantics_UnknownAttributeFormat_IsKeptAsTheBufferFormatValue() + [Theory] + [InlineData(88u, 22u, X001Select)] + [InlineData(257u, 64u, XY01Select)] + [InlineData(298u, 74u, XYZ1Select)] + [InlineData(311u, 77u, IdentitySelect)] + public void ApplySemantics_DecodesPackedFormatsWithChannelSwizzle(uint attributeFormat, uint bufferFormat, uint destinationSelect) { - WriteWord(AttributeTable, AttributeWord(0, 64, 0)); + WriteWord(AttributeTable, AttributeWord(0, attributeFormat, 0)); WriteBuffer(0, StreamBase, 8, 1, BufferFormat32x4Float); var resource = Assert.Single(VertexInputResolver.ApplySemantics(_context, [SemanticWord(0, 0, 2)], AttributeTable, BufferTable, ShaderAddress)); - Assert.Equal(64u, resource.Descriptor.Format); + Assert.Equal(bufferFormat, resource.Descriptor.Format); + Assert.Equal(destinationSelect, resource.Descriptor.DestinationSelectXYZW); } [Theory] @@ -346,13 +354,4 @@ public sealed class VertexInputResolverTests : IDisposable Assert.Contains("records=4/5", fatal.Message); } - [Fact] - public void AttributeFormats_MapToTheBufferFormatsOfTheTable() - { - Assert.Equal(0u, VertexAttributeFormat.ToBufferFormat(0)); - Assert.Equal(1u, VertexAttributeFormat.ToBufferFormat(4)); - Assert.Equal(BufferFormat32x2Float, VertexAttributeFormat.ToBufferFormat(AttributeFormat32x2Float)); - Assert.Equal(BufferFormat32x4Float, VertexAttributeFormat.ToBufferFormat(311)); - Assert.Equal(999u, VertexAttributeFormat.ToBufferFormat(999)); - } } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs index 8ea508c..da77793 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorStateTests.cs @@ -42,7 +42,7 @@ public sealed class RenderExecutorStateTests : IDisposable var state = DrawAndTakeState(banks); Assert.Equal((0f, 64f, 64f, -64f), (state.ViewportX, state.ViewportY, state.ViewportWidth, state.ViewportHeight)); - Assert.Equal((0.5f, 1f), (state.ViewportMinDepth, state.ViewportMaxDepth)); + Assert.Equal((0f, 1f), (state.ViewportMinDepth, state.ViewportMaxDepth)); Assert.Equal(new ScissorRectangle(0, 0, 48, 40), state.Scissor); Assert.Equal(1f, state.LineWidth); Assert.Equal((0.1f, 0.2f, 0.3f, 0.4f), (state.BlendRed, state.BlendGreen, state.BlendBlue, state.BlendAlpha)); @@ -54,6 +54,37 @@ public sealed class RenderExecutorStateTests : IDisposable Assert.Equal((1u, (byte)1), (state.ColorWriteCount, state.ColorWriteEnableMask)); } + [Theory] + [InlineData(false, 0.5f, 0.5f, 0f, 1f)] + [InlineData(true, 0.5f, 0.5f, 0.5f, 1f)] + [InlineData(false, -0.5f, 0.5f, 1f, 0f)] + [InlineData(true, -1f, 1f, 1f, 0f)] + [InlineData(false, 0.25f, 0.5f, 0.25f, 0.75f)] + [InlineData(true, 0.25f, 0.5f, 0.5f, 0.75f)] + [InlineData(false, 0f, 0.5f, 0.5f, 0.5f)] + [InlineData(true, 1f, 0f, 0f, 1f)] + public void DynamicState_DepthRangePreservesTheGuestTransform( + bool directXClipSpace, float depthScale, float depthOffset, float expectedMinimum, float expectedMaximum) + { + var banks = Banks(); + banks.Context.Clip = ClipControlRegisters.Decode(directXClipSpace ? 1u << 19 : 0u); + banks.Context.ScreenViewport.Viewports[0].ZScale = depthScale; + banks.Context.ScreenViewport.Viewports[0].ZOffset = depthOffset; + + var state = DrawAndTakeState(banks); + + Assert.Equal(expectedMinimum, state.ViewportMinDepth); + Assert.Equal(expectedMaximum, state.ViewportMaxDepth); + var hostScale = directXClipSpace + ? state.ViewportMaxDepth - state.ViewportMinDepth + : (state.ViewportMaxDepth - state.ViewportMinDepth) / 2f; + var hostOffset = directXClipSpace + ? state.ViewportMinDepth + : (state.ViewportMaxDepth + state.ViewportMinDepth) / 2f; + Assert.Equal(depthScale, hostScale); + Assert.Equal(depthOffset, hostOffset); + } + [Fact] public void DynamicState_ClipDisableUsesTheDeviceViewportLimit() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs index d563401..4afd7f9 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Rendering/RenderExecutorTestSupport.cs @@ -324,6 +324,8 @@ internal sealed class RecordingRenderHost : IRenderHost public void Dispatch(uint groupsX, uint groupsY, uint groupsZ) => Calls.Add($"dispatch {groupsX} {groupsY} {groupsZ}"); + public bool TryDispatchIndirect(ulong argumentsAddress) => false; + public void ShaderWriteBarrier(PipelineStageFlags sourceStages) => Calls.Add($"write_barrier {sourceStages}"); public void ShaderWriteHazardBarrier() => Calls.Add("write_hazard_barrier"); @@ -335,7 +337,8 @@ internal sealed class RecordingRenderHost : IRenderHost public List<(RegisterBanks Banks, GraphicsPrograms Programs, TargetlessDrawArguments Arguments)> RetainedDraws { get; } = new(); public void ClearColorTargets(ReadOnlySpan targets, SolidColorClear clear) => - Calls.Add($"clear_targets {string.Join(",", targets.ToArray().Select(t => t.Slot))} {clear.Red},{clear.Green},{clear.Blue},{clear.Alpha}"); + Calls.Add(FormattableString.Invariant( + $"clear_targets {string.Join(",", targets.ToArray().Select(t => t.Slot))} {clear.Red},{clear.Green},{clear.Blue},{clear.Alpha}")); public bool TryRetainTargetlessDraw(RegisterBanks banks, GraphicsPrograms programs, in TargetlessDrawArguments arguments) @@ -355,6 +358,9 @@ internal sealed class RecordingRenderHost : IRenderHost public void ResolveImage(ResourceSlotIdentifier source, uint sourceMip, uint sourceLayer, ResourceSlotIdentifier destination, uint destinationMip, uint destinationLayer) => Calls.Add($"resolve {source.Index}:{sourceMip}:{sourceLayer} -> {destination.Index}:{destinationMip}:{destinationLayer}"); + public void CopyDepthStencilImage(ResourceSlotIdentifier source, ResourceSlotIdentifier destination, in SubresourceRange range, in Extent3D extent, ImageAspectFlags aspects) => + Calls.Add($"copy_depth_stencil {source.Index} -> {destination.Index} {aspects}"); + public bool IsMetadata(ulong address) { Calls.Add($"is_metadata {address:X}"); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs index 956e917..a930981 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs @@ -208,6 +208,38 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest private const uint BoundDescriptor = (Format32x4Float << 12) | IdentitySwizzle; private const float Half = 0.5f; + public static IEnumerable DescriptorStoreCases() + { + foreach (var testCase in TypedStoreCases()) + { + var format = (uint)testCase[0]; + testCase[1] = (uint)testCase[1] == 0 ? 0u : (format << 12) | IdentitySwizzle; + yield return testCase; + } + + yield return [50u, (50u << 12) | IdentitySwizzle, 7u, (uint)MemoryOffset, + Floats(1f, Half, 0f, 1f), Bytes(0x03, 0x08, 0xC0, 0xFF)]; + } + + [Theory] + [MemberData(nameof(DescriptorStoreCases))] + public void FormattedBufferStores_UseDescriptorConversionWithoutChangingNeighborBytes( + uint format, uint descriptorWord3, uint opcode, uint offset, uint[] sources, byte[]? expectedBytes) + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + + var initial = CreateInput(); + for (var index = 0; index < sources.Length; index++) + WriteWord(initial, index * 4, sources[index]); + var expected = (byte[])initial.Clone(); + initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64)); + expectedBytes?.CopyTo(expected.AsSpan((int)offset)); + + var shader = CompileTypedShader(format, descriptorWord3, offset, opcode, SourceRegister, typed: false); + Assert.Equal(expected, RunOnce(vulkan, shader, initial)); + } + public static IEnumerable TypedStoreCases() { // instruction format, descriptor word 3, opcode (4 = X .. 7 = XYZW), byte offset, @@ -424,7 +456,7 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset) => CompileTypedShader(instructionFormat, descriptorWord3, offset, opcode: 3, DestinationRegister); - private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister) + private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister, bool typed = true) { const uint descriptorRegister = 16; var words = new List(); @@ -434,9 +466,12 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8)); } var accessPc = checked((uint)words.Count * 4); - // tbuffer_load/store_format_* v[data..], off, s[16:19], 0 format:instructionFormat offset:offset - words.Add(0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset); - words.Add((0x80u << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8)); + // Keep the extended opcode bit clear; use the scalar operand for an odd byte offset. + words.Add(typed + ? 0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset + : 0xE000_0000u | (opcode << 18) | (offset & ~1u)); + var scalarOffsetOperand = typed ? 0x80u : 0x80u + (offset & 1u); + words.Add((scalarOffsetOperand << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8)); for (uint group = 0; group < 4; group++) { words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16)); @@ -447,8 +482,8 @@ public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITest var program = DecodeProgram(words); var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc); var control = Assert.IsType(access.Control); - Assert.True(control.Typed); - Assert.Equal(instructionFormat, control.TypedFormat); + Assert.Equal(typed, control.Typed); + Assert.Equal(typed ? instructionFormat : 0u, control.TypedFormat); Assert.Equal(descriptorRegister, control.ScalarResource); var scalars = BaseScalars(); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs index 4918910..1db325d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs @@ -42,6 +42,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable // Dynamic rendering with the two extended dynamic state extensions, as the presenter's render host needs. public bool SupportsDynamicRendering { get; } + public bool SupportsFragmentShaderBarycentric { get; private init; } private static readonly string[] RenderingExtensionNames = [ @@ -284,6 +285,22 @@ internal sealed unsafe class HeadlessVulkan : IDisposable } } + const string barycentricExtension = "VK_KHR_fragment_shader_barycentric"; + var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR + { + SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr, + }; + if (HasDeviceExtensions(vk, physical, [barycentricExtension])) + { + var query = new PhysicalDeviceFeatures2 + { + SType = StructureType.PhysicalDeviceFeatures2, + PNext = &barycentricFeatures, + }; + vk.GetPhysicalDeviceFeatures2(physical, &query); + } + var barycentric = (bool)barycentricFeatures.FragmentShaderBarycentric; + var extendedDynamicState2Features = new PhysicalDeviceExtendedDynamicState2FeaturesEXT { SType = StructureType.PhysicalDeviceExtendedDynamicState2FeaturesExt, @@ -359,7 +376,15 @@ internal sealed unsafe class HeadlessVulkan : IDisposable PNext = dynamicRendering ? &dynamicRenderingFeatures : null, }; timelineFeatures.PNext = &addressFeatures; - var deviceExtensions = dynamicRendering ? SilkMarshal.StringArrayToPtr(RenderingExtensionNames) : 0; + if (barycentric) + { + barycentricFeatures.PNext = addressFeatures.PNext; + addressFeatures.PNext = &barycentricFeatures; + } + var extensionNames = new List(); + if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames); + if (barycentric) extensionNames.Add(barycentricExtension); + var deviceExtensions = extensionNames.Count > 0 ? SilkMarshal.StringArrayToPtr(extensionNames.ToArray()) : 0; var deviceInfo = new DeviceCreateInfo { SType = StructureType.DeviceCreateInfo, @@ -367,11 +392,11 @@ internal sealed unsafe class HeadlessVulkan : IDisposable QueueCreateInfoCount = 1, PQueueCreateInfos = &queueInfo, PEnabledFeatures = &enabledFeatures, - EnabledExtensionCount = dynamicRendering ? (uint)RenderingExtensionNames.Length : 0u, + EnabledExtensionCount = (uint)extensionNames.Count, PpEnabledExtensionNames = (byte**)deviceExtensions, }; var deviceCreated = vk.CreateDevice(physical, &deviceInfo, null, out var device); - if (dynamicRendering) + if (deviceExtensions != 0) { SilkMarshal.Free(deviceExtensions); } @@ -383,7 +408,10 @@ internal sealed unsafe class HeadlessVulkan : IDisposable } vk.GetDeviceQueue(device, family, 0, out var queue); - var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering); + var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering) + { + SupportsFragmentShaderBarycentric = barycentric, + }; if (validation) { result.RegisterDebugMessenger(); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs index 8b678ec..cfd43c6 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/LayoutImageShaderTests.cs @@ -207,10 +207,10 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO return registers; } - private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1) => + private static DescriptorImageInfo SampledView(CachedImage image, uint levelCount = 1, bool arrayed = false) => new() { - ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount }), + ImageView = image.GetOrCreateView(ImageViewDescription.Default with { Format = image.Backing.Format, Usage = ImageUsageFlags.SampledBit, LevelCount = levelCount, Type = arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D }), ImageLayout = ImageLayout.ShaderReadOnlyOptimal, }; @@ -301,7 +301,12 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO [InlineData(true, false, 2.5f)] [InlineData(false, true, 2.0f)] [InlineData(true, true, 2.5f)] - public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected) + [InlineData(false, false, 1.0f, 9u, 13u, 0u)] + [InlineData(false, false, 2.0f, 9u, 13u, 1u)] + [InlineData(false, false, 1.0f, 13u, 9u, 0u)] + [InlineData(false, false, 2.0f, 13u, 9u, 1u)] + public void IndirectImage_SelectsTheMappedCandidateOnTheDevice(bool loadMip, bool gpuDependentSelector, float expected, + uint firstImageType = 9, uint secondImageType = 9, uint selectedRecord = 1) { var vulkan = fixture.Vulkan; if (!GatePrerequisites.Ready(vulkan)) @@ -309,8 +314,10 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO return; } - var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector); - var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip); + var (request, snapshot) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageRequest(ResultBytes, loadMip, gpuDependentSelector, + firstImageType, secondImageType, firstImageType == 13 || secondImageType == 13 ? 5u : 1u); + var (registers, memory) = SpirvBindingDeclarationTests.IndirectImageAfterPlainImageInputs(ResultBytes, loadMip, firstImageType, secondImageType); + registers[8] = selectedRecord; Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); using var harness = new ImageTestHarness(vulkan); using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); @@ -329,7 +336,7 @@ public sealed class LayoutImageShaderTests(HeadlessVulkanFixture fixture, ITestO images[index] = harness.CreateImage(description); var data = Enumerable.Repeat(texels[index], (int)(levels * levels)).Concat(levels > 1 ? [texels[index] + 0.5f] : Array.Empty()).ToArray(); harness.UploadImage(images[index], MemoryMarshal.AsBytes(data), ImageTestHarness.WholeImageCopies(description, 0)); - infos[index] = [SampledView(images[index], levels)]; + infos[index] = [SampledView(images[index], levels, request.Resources.Info.Images[index].Dimension == ImageDimension.Dim2DArray)]; } Assert.Equal(0x20u, snapshot.Images[1][0]); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs new file mode 100644 index 0000000..701fdf3 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/WaveLaneTransferDeviceTests.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.Libs.Gpu.Buffers; +using SharpEmu.Libs.Tests.Gpu.Images; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.Libs.Tests.Gpu.Vulkan; + +public sealed class WaveLaneTransferDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture +{ + [Theory] + [InlineData(32u, 31u, false)] + [InlineData(32u, 63u, true)] + [InlineData(64u, 31u, false)] + [InlineData(64u, 63u, false)] + [InlineData(64u, 95u, true)] + [InlineData(64u, 127u, true)] + public void ReadLane_UsesGuestWaveIndexAndIgnoresExecutionMask(uint waveSize, uint selector, bool disableExecution) + { + Run(waveSize, selector, disableExecution, writeLane: false); + } + + [Theory] + [InlineData(32u, 63u)] + [InlineData(64u, 31u)] + [InlineData(64u, 63u)] + [InlineData(64u, 127u)] + public void WriteLane_ChangesOnlySelectedGuestLaneWithExecutionDisabled(uint waveSize, uint selector) + { + Run(waveSize, selector, disableExecution: true, writeLane: true); + } + + private void Run(uint waveSize, uint selector, bool disableExecution, bool writeLane) + { + var vulkan = fixture.Vulkan; + if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return; + var instructions = new List(); + uint programCounter = 0; + void Add(Gen5ShaderInstruction instruction) + { + instructions.Add(instruction with { Pc = programCounter }); + programCounter += 8; + } + Add(Vop2(0, "VLshlrevB32", 2, Operand(3), Gen5Operand.Vector(1))); + Add(Vop2(0, "VAddI32", 2, Gen5Operand.Vector(0), Gen5Operand.Vector(2))); + Add(Vop2(0, "VAddI32", 3, Operand(1000), Gen5Operand.Vector(2))); + Add(MoveScalar(0, 20, selector)); + Add(MoveScalar(0, 21, 0x12345678)); + if (disableExecution) + { + Add(MoveScalar(0, 126, 0)); + Add(MoveScalar(0, 127, 0)); + } + if (writeLane) + Add(WriteLane(0, 3, 21, 0) with { Sources = [Gen5Operand.Scalar(21), Gen5Operand.Scalar(20)] }); + else + Add(ReadLane(0, 22, 3, 0) with { Sources = [Gen5Operand.Vector(3), Gen5Operand.Scalar(20)] }); + Add(MoveScalar(0, 126, uint.MaxValue)); + Add(MoveScalar(0, 127, uint.MaxValue)); + if (!writeLane) Add(MoveVectorFromScalar(0, 3, 22)); + Add(Vop2(0, "VLshlrevB32", 5, Operand(2), Gen5Operand.Vector(2))); + Add(BufferAccess(0, "BufferStoreDword", 4, vectorData: 3, offsetEnabled: true, vectorAddress: 5)); + Add(EndProgram(0)); + var (plan, resources, layout) = Prepare(Program([.. instructions])); + var request = new ShaderCompileRequest(plan, resources, layout) + { + LocalSizeX = 8, LocalSizeY = waveSize / 8, ThreadCountX = 8, ThreadCountY = waveSize / 8, + WaveSize = waveSize, + }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + using var harness = new ImageTestHarness(vulkan); + using var runner = new LayoutComputeRunner(harness, request, shader.Spirv); + var output = runner.CreateBuffer(256); + var registers = new uint[256]; + registers[6] = 256; + harness.Run(() => runner.Dispatch(registers, + new Dictionary { [DescriptorBindingKind.Buffers] = [output] }, 1)); + var bytes = runner.ReadBack(output, 0, 256); + var selectedLane = selector % waveSize; + for (uint lane = 0; lane < waveSize; lane++) + { + var expected = writeLane ? (lane == selectedLane ? 0x12345678u : 1000 + lane) : 1000 + selectedLane; + Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan((int)lane * 4))); + } + harness.AssertNoValidationMessages(); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs index fdbd76d..38beda7 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs @@ -27,6 +27,52 @@ public sealed class KernelMemoryCompatExportsTests private const ulong SpanStartOutAddress = GuestMemoryBase + 0x108; private const ulong SpanSizeOutAddress = GuestMemoryBase + 0x110; + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UppercaseStringConversion_ConsumesWideArgumentBetweenNarrowStrings(bool useVaList) + { + var memory = new FakeCpuMemory(GuestMemoryBase, 0x2000); + var context = new CpuContext(memory, Generation.Gen5); + memory.WriteCString(GuestMemoryBase + 0x100, "assets"); + Assert.True(memory.TryWrite(GuestMemoryBase + 0x200, Encoding.Unicode.GetBytes("armor\0"))); + memory.WriteCString(GuestMemoryBase + 0x300, "body"); + const string format = "%s/%S_%s.bin"; + string rendered; + if (useVaList) + { + var arguments = GuestMemoryBase + 0x400; + var savedRegisters = GuestMemoryBase + 0x500; + Assert.True(context.TryWriteUInt64(arguments, 48UL << 32)); + Assert.True(context.TryWriteUInt64(arguments + 8, GuestMemoryBase + 0x600)); + Assert.True(context.TryWriteUInt64(arguments + 16, savedRegisters)); + Assert.True(context.TryWriteUInt64(savedRegisters, GuestMemoryBase + 0x100)); + Assert.True(context.TryWriteUInt64(savedRegisters + 8, GuestMemoryBase + 0x200)); + Assert.True(context.TryWriteUInt64(savedRegisters + 16, GuestMemoryBase + 0x300)); + Assert.True(KernelMemoryCompatExports.TryFormatStringFromVaList(context, format, arguments, out rendered)); + } + else + { + context[CpuRegister.Rcx] = GuestMemoryBase + 0x100; + context[CpuRegister.R8] = GuestMemoryBase + 0x200; + context[CpuRegister.R9] = GuestMemoryBase + 0x300; + rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(context, format, 3); + } + Assert.Equal("assets/armor_body.bin", rendered); + } + + [Theory] + [InlineData("%8.3S", " arm")] + [InlineData("%-8.3S", "arm ")] + public void UppercaseStringConversion_UsesExistingWideStringFormatting(string format, string expected) + { + var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + Assert.True(memory.TryWrite(GuestMemoryBase + 0x100, Encoding.Unicode.GetBytes("armor\0"))); + context[CpuRegister.Rcx] = GuestMemoryBase + 0x100; + Assert.Equal(expected, KernelMemoryCompatExports.FormatStringFromVarArgs(context, format, 3)); + } + [Fact] public void PosixStat_MissingFileReturnsMinusOne() { diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs index b039bcd..988c5dc 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs @@ -45,6 +45,53 @@ public sealed class PresenterImageBindingTests : IClassFixture _vulkan = fixture.Vulkan; + [Theory] + [InlineData(GuestPixelFormat.Bits16UNorm, Format.D16Unorm)] + [InlineData(GuestPixelFormat.Bits32Float, Format.D32Sfloat)] + public void ComparisonBinding_CreatesDepthImageWithoutAnAttachment(GuestPixelFormat guestFormat, Format hostFormat) + { + if (!GatePrerequisites.Ready(_vulkan)) return; + using var fatal = new FatalScope(); + using var presenter = new PresenterUnderTest(_vulkan); + var harness = presenter.Harness; + var address = harness.MapBacked(0x10000, ReadWrite); + var sample = guestFormat == GuestPixelFormat.Bits16UNorm + ? BitConverter.GetBytes((ushort)0x4000) + : BitConverter.GetBytes(0.25f); + var guestBytes = new byte[0x10000]; + for (var offset = 0; offset < guestBytes.Length; offset += sample.Length) + sample.CopyTo(guestBytes, offset); + harness.Write(address, guestBytes); + CachedImage sampledImage = null!; + presenter.Run(() => + { + var resource = new ImageResource + { + ResourceClass = PlanImageResourceClass.Sampled, + NumericClass = ImageNumericClass.Float, + Dimension = ImageDimension.Dim2D, + Read = true, + DepthCompare = true, + }; + var words = RegisterWords.Texture(address, guestFormat, 48, 24); + words[3] = (words[3] & ~0xFFFu) | ViewFormatRules.PackDestinationSelect(4, 0, 0, 1); + var binding = presenter.InvokeMethod("ResolveImageBinding", resource, words, new ShaderProgramInfo(), 0)!; + var identifier = (ResourceSlotIdentifier)GetFieldValue(binding, "ImageIdentifier"); + var request = (ImageRequest)GetFieldValue(binding, "Request"); + var image = harness.Images.GetImage(identifier); + sampledImage = image; + var view = harness.Images.AcquireTextureView(identifier, request); + Assert.Equal(hostFormat, image.Backing.Format); + Assert.True(image.Description.IsDepth); + Assert.NotEqual(0UL, view.Handle); + Assert.Contains(image.Views, candidate => candidate.Description.Aspect == ImageAspectFlags.DepthBit); + presenter.RenderHost.ResetBindings(); + }); + var uploaded = harness.ReadImageBytes(sampledImage, ImageAspectFlags.DepthBit); + Assert.Equal(sample, uploaded.AsSpan(0, sample.Length).ToArray()); + harness.Shutdown(); + } + [Theory] [InlineData(true, 1u, false)] [InlineData(true, 2u, true)] @@ -67,7 +114,7 @@ public sealed class PresenterImageBindingTests : IClassFixture(); presenter.Run(() => { - var shape = new ShaderImageShape(Volume: false, Arrayed: true, Storage: true, DynamicMip: false, TextureNumericClass.Uint); + var shape = new ShaderImageShape(false, true, true, false, TextureNumericClass.Uint); var texture = new GuestDrawTexture(stencilAddress, 64, 64, 0, 0, [], false, true, Descriptor: RegisterWords.Texture(stencilAddress, GuestPixelFormat.Bits8UInt, 64, 64, type: GuestImageType.Color2DArray, tile: GuestTileMode.Depth, layers: layers, baseArray: layers - 1), Shape: shape); diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs index bafab8c..fa8624b 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterUnderTest.cs @@ -53,6 +53,7 @@ internal sealed class PresenterUnderTest : IDisposable SetField("_instance", vulkan.Instance); SetField("_physicalDevice", vulkan.Physical); SetField("_device", vulkan.Device); + SetField("_supportsFragmentShaderBarycentric", vulkan.SupportsFragmentShaderBarycentric); SetField("_deviceInfo", vulkan.DeviceInfo); SetField("_scheduler", Harness.Scheduler); SetField("_relay", Harness.Worker.Relay); diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs new file mode 100644 index 0000000..0ecf4ac --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Interpolation.cs @@ -0,0 +1,115 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE.Host; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.Libs.Gpu.Vulkan; +using SharpEmu.Libs.Tests.Gpu.Images; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; +using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport; +using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed unsafe partial class RenderHostDeviceTests +{ + [Theory] + [InlineData(0u, true, 191)] + [InlineData(1u, true, 64)] + [InlineData(2u, true, 64)] + [InlineData(0u, false, 128)] + [InlineData(1u, false, 0)] + [InlineData(2u, false, 64)] + public void InterpolationParameter_DrawReadsSelectedVertex(uint selector, bool custom, int expected) + { + VerifyInterpolationDraw(selector, custom, interpolate: false, expected); + } + + [Fact] + public void InterpolationPhases_DrawUsesBarycentricRegisters() + { + VerifyInterpolationDraw(2, false, interpolate: true, expected: 96); + } + + [Fact] + public void InterpolationShader_RejectsMissingDeviceFeatureBeforeModuleCreation() + { + if (!Ready()) return; + using var presenter = new PresenterUnderTest(_vulkan); + presenter.SetField("_supportsFragmentShaderBarycentric", false); + var shader = new VulkanCompiledGuestShader(CompileInterpolationFragment(2, true, false)); + presenter.Run(() => + { + var error = Assert.Throws(() => + ((IShaderPipelineHost)presenter.Instance).CreateShaderModule(shader, ShaderStage.Pixel, 1, 1)); + Assert.Contains("fragmentShaderBarycentric", error.Message); + }); + presenter.Harness.Shutdown(); + } + + private void VerifyInterpolationDraw(uint selector, bool custom, bool interpolate, int expected) + { + if (!Ready()) return; + if (!_vulkan.SupportsFragmentShaderBarycentric) + { + Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks fragmentShaderBarycentric."); + Console.Error.WriteLine("[TEST][SKIP] The device lacks fragmentShaderBarycentric."); + return; + } + + using var presenter = new PresenterUnderTest(_vulkan); + presenter.LoadRenderingCommands(); + var harness = presenter.Harness; + var target = harness.MapBacked(0x10000, ReadWrite); + var vertices = harness.MapBacked(0x10000, ReadWrite); + var words = RegisterWords.Color(target, Size, Size); + harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f)); + var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices, + interpolationShader: CompileInterpolationFragment(selector, custom, interpolate)); + var executor = new RenderExecutor(presenter.RenderHost, provider); + presenter.Run(() => executor.DrawAuto(1, Banks(words), Draw())); + presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands")); + harness.Finish(); + var pixels = harness.ReadImageBytes(TargetImage(presenter, words)); + var actual = Pixel(pixels, Size / 2, Size / 2); + Assert.InRange((int)(actual & 255), Math.Max(0, expected - 1), expected + 1); + Assert.Equal(255u, actual >> 24); + harness.Shutdown(); + _vulkan.AssertNoValidationMessages(); + } + + private static byte[] CompileInterpolationFragment(uint selector, bool custom, bool interpolate) + { + var instructions = new List(); + void Parameter(string opcode, uint source) => instructions.Add(new Gen5ShaderInstruction( + (uint)instructions.Count * 4, Gen5ShaderEncoding.Vintrp, opcode, [source], + [Gen5Operand.Vector(source)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(0, 0))); + Parameter("VInterpMovF32", selector); + if (interpolate) + { + Parameter("VInterpP1F32", 0); + Parameter("VInterpP2F32", 1); + } + instructions.Add(MoveVector((uint)instructions.Count * 4, 5, 0x3F800000)); + instructions.Add(new Gen5ShaderInstruction((uint)instructions.Count * 4, Gen5ShaderEncoding.Exp, + "Exp", [], [Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(5)], + [], new Gen5ExportControl(0, 15, false, true, true))); + instructions.Add(EndProgram((uint)instructions.Count * 4)); + var (plan, resources, layout) = Prepare(Program([.. instructions]), ShaderStage.Pixel, userDataCount: 0); + var request = new ShaderCompileRequest(plan, resources, layout) + { + PixelInputAddress = 2, + PixelInputEnable = 2, + PixelInputCntl = [custom ? 0x400u : 0u], + PixelCustomInterpolationMask = custom ? 1u : 0u, + PixelOutputs = [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)], + EnableGraphicsSubgroupOperations = false, + }; + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + return shader.Spirv; + } +} diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs index 1b1427d..704bdae 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.cs @@ -28,7 +28,7 @@ namespace SharpEmu.Libs.Tests.VideoOut; // The executor over the presenter's render host on a real device: each draw renders the guest bytes it was recorded with. [Collection(SchedulingStateCollection.Name)] -public sealed unsafe class RenderHostDeviceTests : IClassFixture +public sealed unsafe partial class RenderHostDeviceTests : IClassFixture { private const uint Size = 64; private const uint VertexStride = 8; @@ -116,7 +116,8 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture _vulkan = fixture.Vulkan; // One float2 position program and one solid red pixel program over empty resource plans. - private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false) : IShaderPipelineProvider + private sealed class FixedProgramProvider(IShaderPipelineHost host, ulong vertexAddress, bool pushData = false, + byte[]? interpolationShader = null) : IShaderPipelineProvider { private const uint Float2Format = 64; private static readonly uint[] UserRegisters = [0, 1]; @@ -151,8 +152,10 @@ public sealed unsafe class RenderHostDeviceTests : IClassFixture + { + var before = GC.GetAllocatedBytesForCurrentThread(); + using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTracking)) + { + } + allocated = GC.GetAllocatedBytesForCurrentThread() - before; + }) { IsBackground = true }; + thread.Start(); + Assert.True(thread.Join(TimeSpan.FromSeconds(30))); + Assert.Equal(0, allocated); + } + + [Fact] + public void DetailMeasurement_PreservesTheOuterScope() + { + Assert.False(RenderPhaseProfile.DetailMeasurementsEnabled); + using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.Draw)) + { + Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled); + using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ImageTracking)) + Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled); + Assert.Equal(RenderPhaseProfile.Enabled, RenderPhaseProfile.DetailMeasurementsEnabled); + } + Assert.False(RenderPhaseProfile.DetailMeasurementsEnabled); + } +} diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs index e59005a..debb298 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs @@ -234,6 +234,60 @@ public sealed class VideoOutFlipRequestTests : IDisposable } } + [Fact] + public void OutputModeEventIsImmediatelyAvailableAndTracksConfiguration() + { + _context[CpuRegister.Rdi] = StatusAddress; + _context[CpuRegister.Rsi] = 0; + Assert.Equal(0, KernelEventQueueCompatExports.KernelCreateEqueue(_context)); + var bytes = new byte[8]; + Assert.True(_memory.TryRead(StatusAddress, bytes)); + var queue = BinaryPrimitives.ReadUInt64LittleEndian(bytes); + try + { + const ulong userData = 0x1234_5678_9ABC_DEF0; + _context[CpuRegister.Rdi] = queue; + _context[CpuRegister.Rsi] = (ulong)_handle; + _context[CpuRegister.Rdx] = userData; + Assert.Equal(0, VideoOutExports.VideoOutAddOutputModeEvent(_context)); + + AssertOutputModeEvent(queue, expectedMode: 1, userData); + + _context[CpuRegister.Rdi] = (ulong)_handle; + _context[CpuRegister.Rsi] = 1; + _context[CpuRegister.Rdx] = 0; + _context[CpuRegister.Rcx] = 0; + _context[CpuRegister.R8] = 0; + Assert.Equal(0, VideoOutExports.VideoOutConfigureOutput(_context)); + + AssertOutputModeEvent(queue, expectedMode: 1, userData); + } + finally + { + _context[CpuRegister.Rdi] = queue; + Assert.Equal(0, KernelEventQueueCompatExports.KernelDeleteEqueue(_context)); + } + } + + private void AssertOutputModeEvent(ulong queue, ulong expectedMode, ulong userData) + { + var timeoutAddress = MemoryBase + 0x400; + Assert.True(_memory.TryWrite(timeoutAddress, BitConverter.GetBytes(1_000_000u))); + _context[CpuRegister.Rdi] = queue; + _context[CpuRegister.Rsi] = MemoryBase + 0x200; + _context[CpuRegister.Rdx] = 1; + _context[CpuRegister.Rcx] = MemoryBase + 0x300; + _context[CpuRegister.R8] = timeoutAddress; + Assert.Equal(0, KernelEventQueueCompatExports.KernelWaitEqueue(_context)); + + var eventBytes = new byte[0x20]; + Assert.True(_memory.TryRead(MemoryBase + 0x200, eventBytes)); + Assert.Equal(8UL, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes)); + Assert.Equal(-13, BinaryPrimitives.ReadInt16LittleEndian(eventBytes.AsSpan(0x08))); + Assert.Equal(expectedMode, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x10)) >> 16); + Assert.Equal(userData, BinaryPrimitives.ReadUInt64LittleEndian(eventBytes.AsSpan(0x18))); + } + [Theory] [InlineData(0)] [InlineData(1)] diff --git a/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs b/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs index d0451a5..6578ad0 100644 --- a/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs @@ -530,7 +530,21 @@ public sealed class MslTranslationTests Assert.DoesNotContain("16u <= sharpemu_resources.buffer_bytes[0] && ", shader.Source, StringComparison.Ordinal); } - private static Gen5MslShader CompileTypedBufferAccess(string opcode, uint dwordCount, uint typedFormat) + [Fact] + public void FormattedBufferStoreUsesDescriptorFormatForPackedNormals() + { + var shader = CompileTypedBufferAccess("BufferStoreFormatXyzw", 4, 0, typed: false, descriptorFormat: 50); + + Assert.Contains("sharpemu_format_encode(v[4], 2u, 0u, 9u)", shader.Source, StringComparison.Ordinal); + Assert.Contains("sharpemu_format_encode(v[5], 10u, 0u, 9u)", shader.Source, StringComparison.Ordinal); + Assert.Contains("sharpemu_format_encode(v[6], 10u, 0u, 9u)", shader.Source, StringComparison.Ordinal); + Assert.Contains("sharpemu_format_encode(v[7], 10u, 0u, 9u)", shader.Source, StringComparison.Ordinal); + Assert.Contains("uint4(0xFFFFFFFFu, 0x0u, 0x0u, 0x0u)", shader.Source, StringComparison.Ordinal); + Assert.DoesNotContain("sharpemu_store_bytes(b", shader.Source, StringComparison.Ordinal); + } + + private static Gen5MslShader CompileTypedBufferAccess(string opcode, uint dwordCount, uint typedFormat, + bool typed = true, uint descriptorFormat = 77) { var data = new Gen5Operand[dwordCount]; for (var index = 0; index < data.Length; index++) @@ -540,7 +554,7 @@ public sealed class MslTranslationTests var access = new Gen5ShaderInstruction( 0, - Gen5ShaderEncoding.Mtbuf, + typed ? Gen5ShaderEncoding.Mtbuf : Gen5ShaderEncoding.Mubuf, opcode, [0, 0], [Gen5Operand.Vector(0), Gen5Operand.Scalar(8), Gen5Operand.Source(128, null)], @@ -555,13 +569,13 @@ public sealed class MslTranslationTests OffsetEnabled: false, Glc: false, Slc: false, - Typed: true, + Typed: typed, TypedFormat: typedFormat)); var end = new Gen5ShaderInstruction(8, Gen5ShaderEncoding.Sopp, "SEndpgm", [0xBF810000], [], [], null); var scalars = new uint[256]; scalars[8] = 0x2000; scalars[10] = 64; - scalars[11] = 77u << 12; + scalars[11] = descriptorFormat << 12; var shader = CompileMaterialized(new Gen5ShaderProgram(0, [access, end]), scalars[..12]); return shader; } diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs index c8e9ecf..faf10c7 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs @@ -19,6 +19,7 @@ public sealed class Gen5Float16ArithmeticTests { var program = Decode( [ + 0x7E00AD01, // v_rsq_f16 v0, v1 0x64000501, // v_add_f16 v0, v1, v2 0x66060B04, // v_sub_f16 v3, v4, v5 0x680C1107, // v_subrev_f16 v6, v7, v8 @@ -29,7 +30,7 @@ public sealed class Gen5Float16ArithmeticTests ]); Assert.Equal( - ["VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"], + ["VRsqF16", "VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"], program.Instructions.Select(instruction => instruction.Opcode)); var request = ResourceTestProgram.Request(program, userDataCount: 0); @@ -45,10 +46,49 @@ public sealed class Gen5Float16ArithmeticTests Assert.Contains((ushort)SpirvOp.FAdd, opcodes); Assert.Contains((ushort)SpirvOp.FSub, opcodes); Assert.Contains((ushort)SpirvOp.FMul, opcodes); - Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 2); + Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 3); Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv)); } + [Fact] + public void Vop3Float16FmaDecodesAndCompiles() + { + var program = Decode( + [ + (0x35u << 26) | (0x34Bu << 16) | 122u, + 261u | (262u << 9) | (263u << 18), + SEndpgm, + ]); + + Assert.Equal("VFmaF16", program.Instructions[0].Opcode); + var request = ResourceTestProgram.Request(program, userDataCount: 0); + Assert.True( + Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), + error); + Assert.Contains((ushort)SpirvOp.ExtInst, ReadOpcodes(shader.Spirv)); + Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv)); + } + + [Fact] + public void PackedFloat16MultiplyAcceptsLiteralOperand() + { + var program = Decode( + [ + (0x33u << 26) | (0x10u << 16) | 1u, + 0xFFu | (258u << 9), + 0x00002C00u, + SEndpgm, + ]); + + Assert.Equal("VPkMulF16", program.Instructions[0].Opcode); + Assert.Equal(Gen5OperandKind.LiteralConstant, program.Instructions[0].Sources[0].Kind); + Assert.Equal(0x00002C00u, program.Instructions[0].Sources[0].Value); + var request = ResourceTestProgram.Request(program, userDataCount: 0); + Assert.True( + Gen5SpirvTranslator.TryCompileProgram(request, out _, out var error), + error); + } + private static Gen5ShaderProgram Decode(IReadOnlyList words) { var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint)); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs index badd32d..1b9c879 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs @@ -66,13 +66,8 @@ public sealed class Gen5ImageTests coordinateOperand: 1, expectedComponents: expectedCoordinateComponents); - var sizeQuery = Assert.Single( - instructions, - item => item.Opcode == SpirvOp.ImageQuerySize); - AssertVectorTypeWidth( - instructions, - sizeQuery.Operands[0], - expectedCoordinateComponents); + // Image stores outside the image bounds are discarded by the host API. + Assert.DoesNotContain(instructions, item => item.Opcode == SpirvOp.ImageQuerySize); } [Fact] diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs new file mode 100644 index 0000000..06845a9 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5InterpolationParameterTests.cs @@ -0,0 +1,149 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Diagnostics; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Tests.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class Gen5InterpolationParameterTests +{ + [Theory] + [InlineData(0u, false, 1u, true)] + [InlineData(1u, false, 2u, true)] + [InlineData(2u, false, 0u, false)] + [InlineData(0u, true, 1u, false)] + [InlineData(1u, true, 2u, false)] + [InlineData(2u, true, 0u, false)] + public void ParameterMove_SelectsVertexAndPreservesCustomData( + uint selector, bool custom, uint vertex, bool subtractOrigin) + { + var request = Request(selector, custom); + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + var instructions = Instructions(shader.Spirv); + Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.Capability && + instruction.Operands[0] == (uint)SpirvCapability.FragmentBarycentricKhr); + var input = Assert.Single(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[1] == (uint)SpirvDecoration.PerVertexKhr).Operands[0]; + Assert.DoesNotContain(instructions, instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[0] == input && instruction.Operands[1] == (uint)SpirvDecoration.Flat); + var firstAccess = instructions.First(instruction => instruction.Opcode == SpirvOp.AccessChain && + instruction.Operands[2] == input); + Assert.Contains(instructions, instruction => instruction.Opcode == SpirvOp.Constant && + instruction.Operands[1] == firstAccess.Operands[3] && instruction.Operands[2] == vertex); + Assert.Equal(subtractOrigin, instructions.Any(instruction => instruction.Opcode == SpirvOp.FSub)); + ValidateWhenAvailable(shader.Spirv); + } + + [Theory] + [InlineData(3u)] + [InlineData(255u)] + public void ReservedSelector_Fails(uint selector) + { + Assert.False(Gen5SpirvTranslator.TryCompileProgram(Request(selector, false), out _, out var error)); + Assert.Contains("reserved interpolation parameter selector", error); + } + + [Fact] + public void MixedBarycentricLocations_ShareBuiltIns() + { + Assert.True(Gen5SpirvTranslator.TryCompileProgram(Request(2, true, 0x77), out var shader, out var error), error); + var builtIns = Instructions(shader.Spirv) + .Where(instruction => instruction.Opcode == SpirvOp.Decorate && + instruction.Operands[1] == (uint)SpirvDecoration.BuiltIn) + .Select(instruction => instruction.Operands[2]).ToArray(); + Assert.Equal(builtIns.Length, builtIns.Distinct().Count()); + Assert.Contains((uint)SpirvBuiltIn.BaryCoordKhr, builtIns); + Assert.Contains((uint)SpirvBuiltIn.BaryCoordNoPerspKhr, builtIns); + Assert.Contains((uint)SpirvBuiltIn.SampleId, builtIns); + ValidateWhenAvailable(shader.Spirv); + } + + [Fact] + public void OrdinaryInterpolation_DoesNotRequireBarycentricFeature() + { + Assert.True(Gen5SpirvTranslator.TryCompileProgram( + Request(0, false, opcode: "VInterpP2F32"), out var shader, out var error), error); + Assert.DoesNotContain(Instructions(shader.Spirv), instruction => instruction.Opcode == SpirvOp.Capability && + instruction.Operands[0] == (uint)SpirvCapability.FragmentBarycentricKhr); + ValidateWhenAvailable(shader.Spirv); + } + + [Fact] + public void PullModel_DoesNotSilentlyUseZeroCoordinates() + { + Assert.False(Gen5SpirvTranslator.TryCompileProgram(Request(2, true, 8), out _, out var error)); + Assert.Contains("Pull-model interpolation", error); + } + + private static ShaderCompileRequest Request( + uint selector, bool custom, uint inputs = 2, string opcode = "VInterpMovF32") + { + var interpolation = new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Vintrp, opcode, + [selector], [Gen5Operand.Vector(selector)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(1, 2)); + var program = ResourceTestProgram.Program(interpolation, ResourceTestProgram.EndProgram(4)); + var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Pixel, userDataCount: 0); + return new ShaderCompileRequest(plan, resources, layout) + { + PixelInputAddress = inputs, + PixelInputEnable = inputs, + PixelInputCntl = [0, 0x401], + PixelCustomInterpolationMask = custom ? 2u : 0u, + }; + } + + private static List Instructions(byte[] code) + { + var words = new uint[code.Length / 4]; + Buffer.BlockCopy(code, 0, words, 0, code.Length); + var result = new List(); + for (var index = 5; index < words.Length;) + { + var count = (int)(words[index] >> 16); + result.Add(new Instruction((SpirvOp)(words[index] & 0xFFFF), words[(index + 1)..(index + count)])); + index += count; + } + return result; + } + + private static void ValidateWhenAvailable(byte[] code) + { + var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK"); + if (string.IsNullOrWhiteSpace(sdk)) + { + return; + } + var executable = Path.Combine(sdk, OperatingSystem.IsWindows() ? "Bin/spirv-val.exe" : "bin/spirv-val"); + if (!File.Exists(executable)) + { + return; + } + var path = Path.GetTempFileName(); + try + { + File.WriteAllBytes(path, code); + var start = new ProcessStartInfo(executable) + { + UseShellExecute = false, + RedirectStandardError = true, + CreateNoWindow = true, + }; + start.ArgumentList.Add("--target-env"); + start.ArgumentList.Add("vulkan1.2"); + start.ArgumentList.Add(path); + using var process = Process.Start(start)!; + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + Assert.True(process.ExitCode == 0, error); + } + finally + { + File.Delete(path); + } + } + + private sealed record Instruction(SpirvOp Opcode, uint[] Operands); +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs index dc5a0f1..5e0669f 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs @@ -150,15 +150,15 @@ public sealed class BindingLayoutTests [Fact] public void ImageBindingAbi() { - Assert.Equal(36u, BindingLayout.ImageBindingCount); + Assert.Equal(42u, BindingLayout.ImageBindingCount); Assert.Equal(0u, (uint)DescriptorBindingKind.Buffers); - Assert.Equal(37u, (uint)DescriptorBindingKind.Samplers); - Assert.Equal(38u, (uint)DescriptorBindingKind.GlobalDataShare); - Assert.Equal(39u, (uint)DescriptorBindingKind.DeviceAddressPageTable); - Assert.Equal(40u, (uint)DescriptorBindingKind.FaultBuffer); - Assert.Equal(41u, (uint)DescriptorBindingKind.FlattenedResourceTable); - Assert.Equal(42u, (uint)DescriptorBindingKind.ShaderData); - Assert.Equal(43u, (uint)DescriptorBindingKind.Count); + Assert.Equal(43u, (uint)DescriptorBindingKind.Samplers); + Assert.Equal(44u, (uint)DescriptorBindingKind.GlobalDataShare); + Assert.Equal(45u, (uint)DescriptorBindingKind.DeviceAddressPageTable); + Assert.Equal(46u, (uint)DescriptorBindingKind.FaultBuffer); + Assert.Equal(47u, (uint)DescriptorBindingKind.FlattenedResourceTable); + Assert.Equal(48u, (uint)DescriptorBindingKind.ShaderData); + Assert.Equal(49u, (uint)DescriptorBindingKind.Count); ImageDimension[] sampledDimensions = [ @@ -170,9 +170,9 @@ public sealed class BindingLayoutTests ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray, ImageDimension.Dim3D, ]; var index = 0u; - void CheckBinding(ImageResourceClass resourceClass, ImageNumericClass numericClass, ImageDimension dimension, bool atomic) + void CheckBinding(ImageResourceClass resourceClass, ImageNumericClass numericClass, ImageDimension dimension, bool atomic, bool cube = false) { - var image = new ImageResource { ResourceClass = resourceClass, NumericClass = numericClass, Dimension = dimension, Atomic = atomic }; + var image = new ImageResource { ResourceClass = resourceClass, NumericClass = numericClass, Dimension = dimension, Atomic = atomic, Cube = cube }; var kind = ImageDescriptorBinding.ForImage(image); Assert.NotNull(kind); Assert.Equal(BindingLayout.FirstImageBinding + index, (uint)kind!.Value); @@ -204,6 +204,12 @@ public sealed class BindingLayoutTests CheckBinding(ImageResourceClass.Storage, ImageNumericClass.Uint, dimension, true); } + foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint, ImageNumericClass.Sint }) + CheckBinding(ImageResourceClass.Sampled, numericClass, ImageDimension.Dim2D, false, cube: true); + foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint }) + CheckBinding(ImageResourceClass.Storage, numericClass, ImageDimension.Dim2D, false, cube: true); + CheckBinding(ImageResourceClass.Storage, ImageNumericClass.Uint, ImageDimension.Dim2D, true, cube: true); + Assert.Equal(BindingLayout.ImageBindingCount, index); static bool Invalid(ImageResource image) => ImageDescriptorBinding.ForImage(image) is null; diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs index 8a2886e..37e4e17 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs @@ -216,9 +216,11 @@ public sealed class DirectImageTableTests }).ToArray(), }; var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); - var candidates = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!.DirectCandidates!; - Assert.Equal(32, candidates.Count); - Assert.DoesNotContain(candidates, candidate => candidate.Offset == 312); + var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!; + Assert.True(selector.Dense); + Assert.Equal(32u, selector.KeyBound); + Assert.Equal(344u, selector.TableOffset); + Assert.True(Assert.Single(plan.IndirectImages).KeyIsAddressOffset); bool Read(ulong address, out uint word) { Assert.True(address >= 0x1000 + 344); @@ -249,9 +251,18 @@ public sealed class DirectImageTableTests ]).ToArray(), }; var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); - var candidates = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!.DirectCandidates!; - Assert.Equal(bypassGuard ? 33 : 32, candidates.Count); - Assert.Equal(bypassGuard, candidates.Any(candidate => candidate.Offset == 312)); + var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!; + if (bypassGuard) + { + Assert.Equal(33, selector.DirectCandidates!.Count); + Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 312); + } + else + { + Assert.True(selector.Dense); + Assert.Equal(32u, selector.KeyBound); + Assert.Equal(344u, selector.TableOffset); + } } [Theory] @@ -304,7 +315,11 @@ public sealed class DirectImageTableTests Instructions = program.Instructions.Select(instruction => instruction.Pc == 40 ? Vop1(40, "VMovB32", 15, Gen5Operand.Scalar(4)) : instruction).ToArray(), }; - Assert.Throws(() => ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2)); + var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2); + Assert.Single(plan.IndirectImages); + Assert.True(plan.Info.UsesDeviceAddresses); + Assert.True(plan.Memory.TryGetIndex(24, 0, out var memoryIndex)); + Assert.False(plan.Memory[memoryIndex].PlanningOnly); } [Theory] diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializerTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializerTests.cs index 52c0cac..693b286 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializerTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializerTests.cs @@ -18,6 +18,8 @@ public sealed class ResourceMaterializerTests [Theory] [InlineData(32)] [InlineData(33)] + [InlineData(64)] + [InlineData(65)] public void MaterialImageCapacityPreservesTheLimitAndPublishedState(int distinctCount) { var plan = Extract(ResourceTrackerTests.IndirectImageProgram(false)); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs index 3f09154..bd5dbbe 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs @@ -12,6 +12,8 @@ namespace SharpEmu.ShaderCompiler.Tests.Resources; public sealed class SpirvBindingDeclarationTests { private const uint ImageType2D = 9; + private const uint ImageTypeCube = 11; + private const uint ImageType2DArray = 13; private static IEnumerable ImageWords(ref uint pc, uint register, uint address, uint format) { @@ -190,7 +192,10 @@ public sealed class SpirvBindingDeclarationTests // A plain sampled image before the indirect root, so the root is image 1 and its second // candidate image 2: the mapping's candidate-local index 1 must select image 2. - internal static Gen5ShaderProgram IndirectImageAfterPlainImageProgram(bool loadMip = false, bool gpuDependentSelector = false) + internal static Gen5ShaderProgram IndirectImageAfterPlainImageProgram( + bool loadMip = false, + bool gpuDependentSelector = false, + uint indirectDimension = 1) { var instructions = new List(); uint pc = 0; @@ -230,7 +235,7 @@ public sealed class SpirvBindingDeclarationTests } else { - Add(Image(pc, "ImageSampleLz", 16, 24, dmask: 1)); + Add(Image(pc, "ImageSampleLz", 16, 24, dimension: indirectDimension, dmask: 1)); } Add(BufferAccess(pc, "BufferStoreDword", 52, 4, 1, vectorData: 4)); @@ -239,7 +244,11 @@ public sealed class SpirvBindingDeclarationTests } // User data and memory for that program: record 1 of the material table selects heap descriptor 1. - internal static (uint[] UserData, TestWordMemory Memory) IndirectImageAfterPlainImageInputs(uint resultBytes, bool twoLevels = false) + internal static (uint[] UserData, TestWordMemory Memory) IndirectImageAfterPlainImageInputs( + uint resultBytes, + bool twoLevels = false, + uint? firstImageType = null, + uint? secondImageType = null) { var userData = new uint[64]; uint[] tables = [0x1000, 224 << 16, 2, 0, 0x2000, 16 << 16, 4, 0, 1]; @@ -251,18 +260,38 @@ public sealed class SpirvBindingDeclarationTests { descriptor[3] |= 1u << 16; } + if (firstImageType is uint rootImageType) + { + descriptor[3] = (descriptor[3] & 0x0FFF_FFFF) | (rootImageType << 28); + } ResourceTrackerTests.WriteImage(memory, 0x2000, descriptor); - ResourceTrackerTests.WriteImage(memory, 0x2020, descriptor); + var secondDescriptor = descriptor.ToArray(); + if (secondImageType is uint imageType) + { + secondDescriptor[3] = (secondDescriptor[3] & 0x0FFF_FFFF) | (imageType << 28); + } + + ResourceTrackerTests.WriteImage(memory, 0x2020, secondDescriptor); memory.At(0x2020) ^= 1; memory.At(0x1000 + 228) = 1; return (userData, memory); } - internal static (ShaderCompileRequest Request, ResourceSnapshot Snapshot) IndirectImageAfterPlainImageRequest(uint resultBytes = 64, bool loadMip = false, bool gpuDependentSelector = false) + internal static (ShaderCompileRequest Request, ResourceSnapshot Snapshot) IndirectImageAfterPlainImageRequest( + uint resultBytes = 64, + bool loadMip = false, + bool gpuDependentSelector = false, + uint? firstImageType = null, + uint? secondImageType = null, + uint indirectDimension = 1) { - var plan = Extract(IndirectImageAfterPlainImageProgram(loadMip, gpuDependentSelector)); - var (userData, memory) = IndirectImageAfterPlainImageInputs(resultBytes, loadMip); + var plan = Extract(IndirectImageAfterPlainImageProgram(loadMip, gpuDependentSelector, indirectDimension)); + var (userData, memory) = IndirectImageAfterPlainImageInputs( + resultBytes, + loadMip, + firstImageType, + secondImageType); var snapshot = new ResourceSnapshot(); var specialization = new ResourceSpecialization(); Assert.True(ResourceMaterializer.Materialize(plan, Inputs(userData, readCleanMemory: memory.Read), ref snapshot, ref specialization)); @@ -292,6 +321,51 @@ public sealed class SpirvBindingDeclarationTests Assert.Contains((ushort)SpirvOp.Select, new SpirvModuleInspector(shader.Spirv).Opcodes); } + [Theory] + [InlineData(ImageType2D, ImageType2DArray)] + [InlineData(ImageType2DArray, ImageType2D)] + public void IndirectImage_Mixed2DAnd2DArrayCandidatesCompileAcrossBindingClasses(uint firstImageType, uint secondImageType) + { + var (request, snapshot) = IndirectImageAfterPlainImageRequest( + firstImageType: firstImageType, secondImageType: secondImageType, indirectDimension: 5); + var root = request.Resources.Info.Images[1]; + var candidate = request.Resources.Info.Images[2]; + Assert.NotEqual(root.Dimension, candidate.Dimension); + Assert.NotEqual(ImageDescriptorBinding.ForImage(root), ImageDescriptorBinding.ForImage(candidate)); + Assert.Equal(firstImageType, snapshot.Images[1][3] >> 28); + Assert.Equal(secondImageType, snapshot.Images[2][3] >> 28); + Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error); + Assert.Contains((ushort)SpirvOp.Select, new SpirvModuleInspector(shader.Spirv).Opcodes); + Assert.False(SharpEmu.ShaderCompiler.Metal.Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError)); + Assert.False(string.IsNullOrWhiteSpace(metalError)); + } + + [Theory] + [InlineData(ImageType2D, ImageTypeCube)] + [InlineData(ImageTypeCube, ImageType2D)] + public void IndirectImage_MixedCubeAnd2DCandidatesAreNotDiscarded( + uint firstImageType, + uint secondImageType) + { + AssertMixedImageCandidatesAreRejected(firstImageType, secondImageType); + } + + private static void AssertMixedImageCandidatesAreRejected(uint firstImageType, uint secondImageType) + { + var plan = Extract(IndirectImageAfterPlainImageProgram(indirectDimension: 3)); + var (userData, memory) = IndirectImageAfterPlainImageInputs( + 64, false, firstImageType: firstImageType, secondImageType: secondImageType); + var snapshot = new ResourceSnapshot(); + var specialization = new ResourceSpecialization(); + var previousSnapshot = snapshot; + var previousSpecialization = specialization; + Assert.False(ResourceMaterializer.Materialize(plan, Inputs(userData, readCleanMemory: memory.Read), + ref snapshot, ref specialization, out var failure)); + Assert.Equal(ResourceMaterializationFailure.IncompatibleImageCandidates, failure); + Assert.Same(previousSnapshot, snapshot); + Assert.Same(previousSpecialization, specialization); + } + [Fact] public void GlobalDataShareAppendInAPixelStage_BroadcastsTheCounter() {