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();
+ 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 GuestReads { get; } = new();
+ public Action? BeforeGuestRead { get; set; }
+
public List IndexedDraws { get; } = new();
public List AutoDraws { get; } = new();
@@ -55,6 +57,7 @@ internal sealed class RecordingCommandStreamHost : ICommandStreamHost
public bool TryReadGuest(ulong address, Span destination)
{
GuestReads.Add(address);
+ BeforeGuestRead?.Invoke(address);
if (PendingGpuValues.Remove(address, out var pending))
{
WriteQword(address, pending);