From 4eea0075f80f7ebed42f5682a3a344f3f5b47bdc Mon Sep 17 00:00:00 2001
From: ParantezTech <12572227+par274@users.noreply.github.com>
Date: Fri, 2 Oct 2026 19:06:28 +0300
Subject: [PATCH] [GpuCommands] Let a wait see its own queue's preceding label
store
A memory wait that directly follows a 4- or 8-byte store of its queue to the
same address, with only waits or inert NOPs and markers between them, compares
against the stored value when guest memory no longer holds it. Dead Cells
writes a label and waits for it while the CPU already rewrites it for the next
frame. The forwarded value lives only within one slice, and any draw, dispatch
or transfer in between drops it.
---
docs/sharpemu-gui-undocumented-env-vars.md | 2 +-
.../GpuCommandInterpreter.Waits.cs | 5 ++
.../Gpu/GpuCommands/GpuCommandInterpreter.cs | 42 +++++++++++
.../GpuCommands/CommandStreamQueueTests.cs | 73 +++++++++++++++++++
.../GpuCommands/CommandStreamTestSupport.cs | 3 +
5 files changed, 124 insertions(+), 1 deletion(-)
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);