From 0286a767e213cbfb5cef72b367f1e09e39bae80e Mon Sep 17 00:00:00 2001
From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:16:17 -0400
Subject: [PATCH] feat(agc): capture submissions and replay their decode and
wait releases offline
A hang with threads in Driver::waitMemory takes a ten-minute boot to
reproduce, and the existing switches show only the first submissions of
one queue (APS5_DUMP_QUEUE) or a rejected one (APS5_DUMP_REJECTED).
Capture. APS5_DUMP_SUBMITS=
writes every submission the driver
accepts. /index.txt starts with "aps5-submissions 1" and "ordinal
queue type serial time_us dwords kind file held conditional", then one
line per submission: acceptance ordinal, queue id, graphics or compute,
serial, microseconds on the APS5_TRACE_GPU clock, dword count, kind,
file name, the dword offsets of the waits held at submit
(Submission::heldAtSubmit) and the COND_EXEC and predicated indirect
buffer ranges (Submission::conditionalEnds) as start:end, "-" for none.
/submission_.bin holds the copied dwords, indirect
buffers inlined. REWIND tails are listed as rewind with the serial of
their submission, suspend points as suspend without a file.
SubmissionCapture::Prepare copies the commands before the driver lock;
Take, under it after reserveOutputs, sets only the ordinal, time and
held waits and does not allocate; the file and then its index line are
written after the lock. A failed write reports the failure to the driver
(Driver::ReportFailure) and throws, so every later driver call throws
too. Unset, Submit tests a static pointer. An index written before the
held and conditional columns ("ordinal queue type serial time_us dwords
kind file") is read with both unknown.
Analysis. agc_pm4_replay [--full] decodes each submission and
throws on a packet the driver rejects (a corrupt capture). It does not
model cross-queue timing: a wait is released only when the capture
alone proves that the driver releases it (WaitMemory.cpp,
LabelHistory.cpp), and unknown, with the reason, when its outcome
depends on when another queue's stores run. A memory wait is released
when its compare always passes, when it was held at submit, when the
last store to the awaited bytes before it on its queue covers them with
an accepted value, is not conditional and no other queue stores to
those bytes anywhere in the capture, or when a RELEASE_MEM or
WRITE_DATA label earlier in the waiting submission that the label
history keeps (KeepsLabelStore, not conditional) stores an accepted
value and fewer than LabelStoreHistory other stores to the awaited
bytes can run between it and the wait: those between them on the
waiting queue and every store of another queue to those bytes, since
the history keeps the last stores per dword over all queues and finds
only a store that ran after the waiting submission entered the driver.
A REWIND is released when its tail is in the capture; one valid at
submit without a captured tail is unknown, because the driver reads the
valid bit again when the queue reaches it. A wait is unreleased when it was not held at submit and no
store that can run before it may write an accepted value (a store of
part of the awaited bytes or of a run-time value may; a store later on
its queue runs after it): the GPU does not release it, a CPU or shader
store could. A REWIND without a tail that was not valid at submit is unreleased. Every other wait is
unknown: "depends on cross-queue timing (stores on queue X at #...)"
when another queue stores to the awaited bytes, otherwise because the
last store before it on its queue is conditional, stores a run-time
value or part of the bytes, or overwrites an accepted store the capture
does not prove still in the label history. Without the held column a
wait that would be unreleased is unknown; without the conditional
column every packet after a COND_EXEC or predicated INDIRECT_BUFFER is
conditional to the end of its submission. The outcomes depend on the
order of each queue's stores and on which stores other queues make, not
on the submit order across queues. The report counts the outcomes,
routes and cross-queue unknowns, says when the index lacks the columns,
lists up to 32 unreleased and then unknown waits newest first with the
packets around them and their stores, then the last submissions of
each queue; exit code 1 when a wait is unreleased, 2 on errors.
Pm4 gains DecodeStores, DecodeWait, DecodeConditionalWrite,
DecodeConstantRamDump, the COPY_DATA and DMA_DATA field helpers, the
sample dump layout and RewindValid; Execute, ResolveStore, Validate,
DecodeMemoryCopy, the driver's sample dump, noteHeldAtSubmit and
executeRewindTail use them. LabelStoreHistory and KeepsLabelStore move
to LabelStores.hpp, used by noteLabelStore and the analysis.
Test. agc_driver_pm4_replay submits command buffers built with the
libSceAgc builders through the driver with the switch set and checks the
index, the held waits, the COND_EXEC range, the dwords, the decode and
the outcome of eight waits, including WRITE_DATA 1, WRITE_DATA 0,
WAIT == 1 in one submission, which the driver releases through its
label history. Hand-made captures check each release route, a store of
another queue to the awaited bytes that entered before or after
(unknown), the label history limit with later stores on the waiting
queue or on another queue, a fill the history does not keep, a store
in an earlier submission overwritten later, later stores, rejected
values and a missing rewind tail (unreleased), the six captures of the
reviews (none released but the wait held at submit), five groups of
waits in eight submission orders with identical outcomes, an index
without the held and conditional columns and corrupt captures. A
directory in place of the next submission file makes the write fail,
and the submit, the next submit and shutdown throw.
agc_driver_pm4_replay_tool runs the tool through
tests/Pm4ReplayTool.cmake and checks exit codes 1 and 2.
Co-Authored-By: Claude Opus 5.5
---
core/libs/prx/libSceAgcDriver/CMakeLists.txt | 32 +
.../Driver/Synchronization/LabelStores.hpp | 18 +
.../Driver/Synchronization/WaitMemory.hpp | 3 +-
.../libSceAgcDriver/Execution/include/Pm4.hpp | 47 ++
.../Execution/include/SubmissionCapture.hpp | 68 ++
.../src/Driver/Packets/PacketExecution.cpp | 2 +-
.../src/Driver/Packets/PacketMemory.cpp | 8 +-
.../src/Driver/Queues/Submission.cpp | 43 +-
.../Driver/Synchronization/LabelHistory.cpp | 2 +-
.../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 146 +++-
.../Execution/src/SubmissionCapture.cpp | 237 ++++++
.../Execution/src/VulkanDevice.cpp | 2 +-
.../prx/libSceAgcDriver/tests/Pm4Replay.cpp | 730 +++++++++++++++++
.../libSceAgcDriver/tests/Pm4ReplayTool.cmake | 29 +
.../prx/libSceAgcDriver/tools/Pm4Analysis.cpp | 765 ++++++++++++++++++
.../prx/libSceAgcDriver/tools/Pm4Analysis.hpp | 91 +++
.../prx/libSceAgcDriver/tools/Pm4Replay.cpp | 35 +
docs/dev/BUILD.md | 13 +
18 files changed, 2222 insertions(+), 49 deletions(-)
create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp
create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp
create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp
create mode 100644 core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp
create mode 100644 core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake
create mode 100644 core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.cpp
create mode 100644 core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.hpp
create mode 100644 core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp
diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt
index 3fdab7d9a8..b4d09e16fa 100644
--- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt
+++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt
@@ -171,6 +171,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubgroupClock.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderDeviceProfile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubmissionCapture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp
@@ -1558,6 +1559,37 @@ endif()
set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_replay)
+add_executable(agc_pm4_replay EXCLUDE_FROM_ALL
+ tools/Pm4Replay.cpp
+ tools/Pm4Analysis.cpp
+)
+target_include_directories(agc_pm4_replay PRIVATE ${LIBS_INCLUDE_DIR})
+target_link_libraries(agc_pm4_replay PRIVATE libSceAgcDriver libc)
+set_target_properties(agc_pm4_replay PROPERTIES CXX_EXTENSIONS OFF)
+configure_windows_unwind(agc_pm4_replay)
+
+if(BUILD_TESTING)
+ add_test_executable(agc_driver_pm4_replay_tests tests/Pm4Replay.cpp tools/Pm4Analysis.cpp)
+ target_include_directories(agc_driver_pm4_replay_tests PRIVATE ${LIBS_INCLUDE_DIR})
+ target_link_libraries(agc_driver_pm4_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc)
+ set_target_properties(agc_driver_pm4_replay_tests PROPERTIES CXX_EXTENSIONS OFF)
+ configure_windows_unwind(agc_driver_pm4_replay_tests)
+ add_dependencies(agc_driver_pm4_replay_tests agc_pm4_replay)
+ set(agcPm4ReplayCapture "${CMAKE_CURRENT_BINARY_DIR}/pm4-replay-capture")
+ add_test(NAME agc_driver_pm4_replay COMMAND agc_driver_pm4_replay_tests)
+ set_tests_properties(agc_driver_pm4_replay PROPERTIES
+ ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DUMP_SUBMITS=${agcPm4ReplayCapture}"
+ FIXTURES_SETUP agc_pm4_replay_capture TIMEOUT 30)
+ add_test(NAME agc_driver_pm4_replay_tool COMMAND ${CMAKE_COMMAND} -DTOOL=$ "-DCAPTURE=${agcPm4ReplayCapture}"
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/Pm4ReplayTool.cmake)
+ set_tests_properties(agc_driver_pm4_replay_tool PROPERTIES
+ FIXTURES_REQUIRED agc_pm4_replay_capture TIMEOUT 30)
+ if(WIN32)
+ set_tests_properties(agc_driver_pm4_replay agc_driver_pm4_replay_tool PROPERTIES ENVIRONMENT_MODIFICATION
+ "PATH=path_list_prepend:$")
+ endif()
+endif()
+
add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp)
target_include_directories(agc_compute_pipeline_replay PRIVATE
${LIBS_INCLUDE_DIR}
diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp
new file mode 100644
index 0000000000..dc39feeb1a
--- /dev/null
+++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp
@@ -0,0 +1,18 @@
+#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELSTORES_HPP
+#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELSTORES_HPP
+
+#include
+#include
+
+namespace AgcDriver::DriverDetail {
+
+inline constexpr std::size_t LabelStoreHistory = 4;
+inline constexpr std::size_t LabelStoreBytes = 64;
+
+inline bool KeepsLabelStore(std::uint64_t address, std::size_t bytes) {
+ return bytes != 0 && bytes <= LabelStoreBytes && bytes % 4 == 0 && address % 4 == 0;
+}
+
+}
+
+#endif
diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp
index dc1e3b7890..8157279e9e 100644
--- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp
+++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_WAITMEMORY_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_WAITMEMORY_HPP
+#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include
#include
@@ -9,8 +10,6 @@
namespace AgcDriver::DriverDetail {
-inline constexpr std::size_t LabelStoreHistory = 4;
-
inline constexpr const char* LabelRefusalNames[6] = {"none", "trust-off", "queued", "overwritten", "unclosed", "behind-completion"};
inline constexpr std::chrono::microseconds PollTryInterval{1000};
diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp
index ce6f1638eb..35c5467efb 100644
--- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp
+++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp
@@ -142,6 +142,53 @@ struct MemoryCopy {
std::optional DecodeMemoryCopy(std::span packet);
std::uint32_t DmaSource(std::span packet);
std::uint32_t DmaDestination(std::span packet);
+inline constexpr std::uint32_t DmaSelectGds = 1;
+inline constexpr std::uint32_t DmaSourceData = 2;
+inline constexpr std::uint32_t DmaDestinationNowhere = 2;
+inline std::size_t DmaBytes(std::span packet) { return packet[6] & 0x3ffffffu; }
+inline constexpr std::uint32_t CopyDataGpuClockSource = 18;
+inline std::uint32_t CopyDataSource(std::span packet) { return ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u); }
+inline bool CopyDataImmediate(std::uint32_t source) { return source >= 10 && source != CopyDataGpuClockSource; }
+inline bool CopyDataToRegister(std::span packet) { return ((packet[1] >> 8u) & 0xfu) == 0; }
+inline std::size_t CopyDataBytes(std::span packet) { return (packet[1] & 0x10000u) != 0 ? 8 : 4; }
+inline constexpr std::uint32_t RewindValid = 0x80000000u;
+struct WaitFields {
+ std::uint32_t function;
+ std::uint64_t address;
+ std::uint64_t reference;
+ std::uint64_t mask;
+ std::uint32_t poll;
+ std::size_t bytes;
+};
+WaitFields DecodeWait(std::span packet);
+struct ConditionalWrite {
+ std::uint64_t compared;
+ std::uint64_t destination;
+ std::uint32_t value;
+};
+ConditionalWrite DecodeConditionalWrite(std::span packet);
+struct ConstantRamDump {
+ std::uint32_t offset;
+ std::uint32_t words;
+ std::uint64_t destination;
+};
+ConstantRamDump DecodeConstantRamDump(std::span packet);
+inline constexpr std::uint32_t SampleDumpEvent = 0x39;
+inline constexpr std::uint64_t SampleDumpBlocks = 16;
+inline constexpr std::uint64_t SampleDumpStride = 16;
+inline constexpr std::uint64_t SampleDumpBlockBytes = 8;
+inline constexpr std::uint64_t SampleDumpBytes = (SampleDumpBlocks - 1) * SampleDumpStride + SampleDumpBlockBytes;
+inline bool SampleDump(std::span packet) { return ((packet[0] >> 8u) & 0xffu) == 0x46u && packet.size() >= 2 && (packet[1] & 0x3fu) == SampleDumpEvent; }
+inline std::uint64_t SampleDumpAddress(std::span packet) { return packet[2] | (static_cast(packet[3]) << 32u); }
+struct PacketStore {
+ enum class Value : std::uint8_t { Bytes, Pattern, RunTime };
+ std::uint64_t address = 0;
+ std::uint64_t bytes = 0;
+ Value value = Value::RunTime;
+ std::vector data;
+ bool conditional = false;
+};
+std::vector DecodeStores(std::span packet);
// A DISPATCH_INDIRECT's arguments: the guest address of its three group-count dwords (no memory
// access, so the GPU can read them in place: VulkanDevice::DispatchIndirect), the DISPATCH_DIRECT
// packet made by reading them there (through the checked guest memory path, which waits for
diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp
new file mode 100644
index 0000000000..1db347dcca
--- /dev/null
+++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp
@@ -0,0 +1,68 @@
+#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBMISSIONCAPTURE_HPP
+#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBMISSIONCAPTURE_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include