diff --git a/docs/sharpemu-gui-undocumented-env-vars.md b/docs/sharpemu-gui-undocumented-env-vars.md index e785001..0c71cd9 100644 --- a/docs/sharpemu-gui-undocumented-env-vars.md +++ b/docs/sharpemu-gui-undocumented-env-vars.md @@ -126,7 +126,7 @@ These variables change the GPU path or the Vulkan presenter. | `SHARPEMU_SHADER_MAX_STEPS` | number | Set the maximum number of dispatcher loop steps in each translated shader invocation. The limit makes sure that an incorrect loop stops. `0` removes the limit. The default is 100000. | `Gen5SpirvTranslator.cs`, `Gen5MslTranslator.cs` | | `SHARPEMU_SKIP_ALL_COMPUTE` | `1` | Set to `1` to make the Metal presenter skip all compute dispatches. The Vulkan presenter reads the value but does not use it. The default is off. | `MetalVideoPresenter.Compute.cs`, `VulkanVideoPresenter.cs` | | `SHARPEMU_SKIP_TALL_COMPUTE_Z` | number | Effect not clear from the code. The Vulkan presenter reads the number into a field for a minimum Z group count. No code uses that field. The default is 0. | `VulkanVideoPresenter.cs` | -| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. | `CommandStreamQueue.cs` | +| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. A wait that follows its own queue's store to the same label still sees that value when the title rewrites the label for the next frame (Dead Cells). | `CommandStreamQueue.cs`, `GpuCommandInterpreter.cs` | | `SHARPEMU_VK_DEBUG_LABELS` | `1` | Turns on Vulkan object names and command labels for capture tools. `SHARPEMU_VK_VALIDATION=1` also turns them on. Default is off because the labels add overhead to each draw. | `VulkanVideoPresenter.Draws.Recording.cs` | | `SHARPEMU_VK_DEVICE` | text (part of a device name) | Selects the Vulkan device whose name contains this text. The comparison ignores case. When unset, the presenter gives a score to each device and prefers a discrete GPU. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.cs` | | `SHARPEMU_VK_PIPELINE_CACHE_PATH` | path | Sets the path of the Vulkan pipeline cache file. The path can contain environment variables. Default is `user/pipeline_cache//` below the application directory. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.cs` | diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs index 3d276c5..7e6f8fd 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Waits.cs @@ -34,6 +34,11 @@ public sealed partial class GpuCommandInterpreter throw _host.Fatal($"The wait compare function is unknown: function={compareFunction} address=0x{address:X16}."); } + if (!satisfied && TryReadOwnWrite(address, is64Bit, out var ownValue)) + { + _ = WaitOperation.TryCompare(ownValue, reference, mask, compareFunction, out satisfied); + } + if (!satisfied) { Suspend(); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs index 19ac605..ef91e56 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.cs @@ -32,6 +32,11 @@ public sealed partial class GpuCommandInterpreter private readonly uint[] _constantRam = new uint[ConstantRamDwords]; private PacketCursorStack? _execution; private bool _chainRequested; + private ulong _packetSerial; + private ulong _lastWriteAddress; + private ulong _lastWriteValue; + private int _lastWriteLength; + private ulong _lastWritePacket; public GpuCommandInterpreter(ICommandStreamHost host, int queueId, int interruptEventId) { @@ -119,6 +124,7 @@ public sealed partial class GpuCommandInterpreter public void Reset() { + _lastWriteLength = 0; Registers.Reset(); TypedRegisters.Reset(); IndexTypeAndSize = 0; @@ -163,6 +169,8 @@ public sealed partial class GpuCommandInterpreter execution.Suspended = false; execution.MadeProgress = false; + // Forwarding is local to this slice, never a cached signal from an older submission. + _lastWriteLength = 0; _execution = execution; try { @@ -206,6 +214,20 @@ public sealed partial class GpuCommandInterpreter public void Suspend() => RequireExecution().Suspended = true; + // A paired wait must still observe this queue's immediate label store when the CPU + // resets the live address for the next frame between the two packets (Dead Cells). + private bool TryReadOwnWrite(ulong address, bool is64Bit, out ulong value) + { + value = 0; + if (address != _lastWriteAddress || _packetSerial - _lastWritePacket > 2 || + _lastWriteLength < (is64Bit ? sizeof(ulong) : sizeof(uint))) + { + return false; + } + value = is64Bit ? _lastWriteValue : (uint)_lastWriteValue; + return true; + } + private PacketCursorStack RequireExecution() => _execution ?? throw _host.Fatal($"No command stream is running: queue={QueueId}."); @@ -242,6 +264,7 @@ public sealed partial class GpuCommandInterpreter var packetAddress = cursor.Address + ((ulong)cursor.Offset * sizeof(uint)); var header = ReadDword(packetAddress, RenderPhaseProfile.CommandReadKind.Header); + _packetSerial++; var total = cursor.DwordCount; var remaining = cursor.Remaining; var offset = cursor.Offset; @@ -279,6 +302,15 @@ public sealed partial class GpuCommandInterpreter } var opcode = PacketHeader.Opcode(header); + // Only a wait or an inert NOP/marker may separate the store and its paired wait. + // Draws, dispatches and transfers may produce a newer GPU value at the same address. + if (opcode is not (PacketOpcode.WaitRegisterMemory or PacketOpcode.WaitRegisterMemory64) && + !(opcode == PacketOpcode.Nop && PacketHeader.CustomCode(header) is + 0 or PacketCustomCode.PushMarker or PacketCustomCode.PopMarker or + PacketCustomCode.WaitMemory32 or PacketCustomCode.WaitMemory64)) + { + _lastWriteLength = 0; + } var handler = PacketDispatchTable.Opcodes[opcode]; if (handler is null) { @@ -411,6 +443,16 @@ public sealed partial class GpuCommandInterpreter { throw _host.Fatal($"The command stream cannot write guest memory: address=0x{address:X16} size={source.Length}."); } + _lastWriteLength = 0; + if (source.Length is sizeof(uint) or sizeof(ulong)) + { + _lastWriteAddress = address; + _lastWriteValue = source.Length == sizeof(uint) + ? BinaryPrimitives.ReadUInt32LittleEndian(source) + : BinaryPrimitives.ReadUInt64LittleEndian(source); + _lastWriteLength = source.Length; + _lastWritePacket = _packetSerial; + } } private static ulong Address(uint low, uint high) => low | ((ulong)high << 32); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs index 7052b9c..5e461d1 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs @@ -24,6 +24,9 @@ public sealed class CommandStreamQueueTests private static uint[] WaitEqual(ulong address, uint reference) => StreamRunner.Packet(PacketOpcode.WaitRegisterMemory, 0x13u, StreamRunner.Low(address), StreamRunner.High(address), reference, 0xFFFF_FFFFu, 0); + private static uint[] WriteLabel(ulong address, uint value) => + StreamRunner.Packet(PacketOpcode.WriteData, 0x00000500u, StreamRunner.Low(address), StreamRunner.High(address), value); + private static (RecordingCommandStreamHost Host, CommandStreamQueue Queue) NewQueue() { var host = new RecordingCommandStreamHost(); @@ -427,4 +430,74 @@ public sealed class CommandStreamQueueTests Assert.Equal(SliceResult.Completed, queue.ProcessOne()); Assert.Equal(IdleOutcome.Completed, await guest.WaitAsync(TimeSpan.FromSeconds(2))); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task PairedLabelWait_SurvivesTheNextFramesResetWithRunAhead(bool is64Bit) + { + var host = new RecordingCommandStreamHost(); + var queue = new CommandStreamQueue(host, boundariesInFlight: 1); + var packets = new List<uint[]>(); + for (var pair = 0; pair < 7; pair++) + { + var address = Label + (ulong)pair * 8; + packets.Add(is64Bit + ? StreamRunner.Packet(PacketOpcode.WriteData, 0x00000500u, + StreamRunner.Low(address), StreamRunner.High(address), 1, 2) + : WriteLabel(address, 1)); + packets.Add(is64Bit + ? StreamRunner.Packet(PacketOpcode.WaitRegisterMemory64, 0x13u, + StreamRunner.Low(address), StreamRunner.High(address), 1, 2, uint.MaxValue, uint.MaxValue, 0) + : WaitEqual(address, 1)); + } + Enqueue(host, queue, Graphics, 1, packets.ToArray()); + host.BeforeGuestRead = address => + { + if (address == Label + 6 * 8) host.WriteQword(address, 0); + }; + + Assert.Equal(IdleOutcome.Completed, await Task.Run(queue.Done).WaitAsync(TimeSpan.FromSeconds(2))); + Assert.Equal(0, queue.FrameNumber); // CPU/GPU overlap remains enabled. + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(0UL, host.ReadQword(Label + 6 * 8)); // The CPU reset was not overwritten. + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(1, queue.FrameNumber); + Assert.Equal(0, queue.BlockedQueueCount); + } + + [Theory] + [InlineData(0)] // A CPU-produced label must still be read from live memory. + [InlineData(1)] // Never forward a value from the preceding submission. + [InlineData(2)] // Never forward through work that may write the label on the GPU. + public void LabelWait_DoesNotForwardAnUnrelatedOrOlderStore(int scenario) + { + var (host, queue) = NewQueue(); + host.WriteDword(Label, 0); + if (scenario == 0) + { + Enqueue(host, queue, Graphics, 1, WriteLabel(Label + 8, 1), WaitEqual(Label, 1)); + } + else if (scenario == 1) + { + Enqueue(host, queue, Graphics, 1, WriteLabel(Label, 1)); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + host.WriteDword(Label, 0); + Enqueue(host, queue, Graphics + 0x100, 2, WaitEqual(Label, 1)); + } + else + { + Enqueue(host, queue, Graphics, 1, WriteLabel(Label, 1), + StreamRunner.Packet(PacketOpcode.DrawIndexAuto, 3, 0), WaitEqual(Label, 1)); + host.BeforeGuestRead = address => + { + if (address == Label) host.WriteDword(Label, 0); + }; + } + Assert.Equal(scenario == 1 ? SliceResult.BlockedWithoutProgress : SliceResult.Progressed, queue.ProcessOne()); + host.BeforeGuestRead = null; + host.WriteDword(Label, 1); + queue.RetryBlocked(); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + } } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs index 44e7270..c61697d 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamTestSupport.cs @@ -33,6 +33,8 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost public List<ulong> GuestReads { get; } = new(); + public Action<ulong>? BeforeGuestRead { get; set; } + public List<DrawIndexedArguments> IndexedDraws { get; } = new(); public List<DrawAutoArguments> AutoDraws { get; } = new(); @@ -55,6 +57,7 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost public bool TryReadGuest(ulong address, Span<byte> destination) { GuestReads.Add(address); + BeforeGuestRead?.Invoke(address); if (PendingGpuValues.Remove(address, out var pending)) { WriteQword(address, pending);