[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.
This commit is contained in:
parent
72eb9dbfd9
commit
4eea0075f8
|
|
@ -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/<title id>/` below the application directory. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue