Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d64657c
feat(agc): replay a capture several times in one process
Sam-mon7 Oct 21, 2026
8a17ba3
fix(agc): keep draining a replay until no submission completes for te…
Sam-mon7 Oct 21, 2026
f691bc9
fix(agc): replay frames with the title's pipeline cache and journal a…
Sam-mon7 Oct 21, 2026
5584210
fix(agc): replay review fixes: inherited pipeline cache, shell quotin…
Sam-mon7 Oct 27, 2026
0c8232d
feat(agc): trace the serials the driver accepts and completes under A…
Sam-mon7 Oct 21, 2026
c793788
feat(agc): list a capture's shaders and submissions and trace a repla…
Sam-mon7 Oct 21, 2026
e6b0a9b
fix(agc): replay listing review fixes: non-Windows packet decoding, p…
Sam-mon7 Oct 27, 2026
210bd86
test(agc): accept the host CPU line in the late shader output failure…
Sam-mon7 Oct 28, 2026
df74f29
fix(agc): relaunch the frame replay when a host region blocks a captu…
chitters2004 Oct 29, 2026
89db2b5
docs(env): list APS5_FRAME_REPLAY_ATTEMPT
chitters2004 Oct 29, 2026
9392228
Merge upstream main 466bf10f7f into feat/frame-replay-full-titles
Sam-mon7 Oct 29, 2026
752958c
Merge feat/frame-replay-full-titles (upstream main 466bf10f7f) into f…
Sam-mon7 Oct 29, 2026
501b4fd
Merge #11436 (752958c135) into fix/frame-replay-relaunch-host-collision
chitters2004 Oct 29, 2026
cd69be5
Merge main into fix/frame-replay-relaunch-host-collision
chitters2004 Oct 31, 2026
93123ef
test(agc): skip the frame replay tests without a Vulkan driver
chitters2004 Oct 31, 2026
9f770fd
Merge branch 'main' of https://github.com/boykopovar/AnyPS5 into HEAD
chitters2004 Oct 31, 2026
f2ba0a7
Merge remote-tracking branch 'origin/main' into fix/frame-replay-rela…
chitters2004 Nov 1, 2026
9a2b40f
fix(agc): bound the frame replay relaunch count and test the relaunch…
chitters2004 Nov 1, 2026
4c144e8
Merge remote-tracking branch 'origin/main' into fix/frame-replay-rela…
chitters2004 Nov 1, 2026
d180980
Merge branch 'main' into fix/frame-replay-relaunch-host-collision
chitters2004 Nov 2, 2026
c4f735f
Merge branch 'main' into fix/frame-replay-relaunch-host-collision
chitters2004 Nov 2, 2026
6fb0739
Merge branch 'main' into fix/frame-replay-relaunch-host-collision
chitters2004 Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extern "C" void AgcDriverRequestScreenshot_nid_postfix();
extern "C" void AgcDriverSuspendPoint_nid_postfix();
extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std::uint64_t* completed);
extern "C" std::size_t AgcDriverAwaitedWaits_nid_postfix(AgcDriver::AwaitedWait* waits, std::size_t capacity);
extern "C" void AgcDriverWaitForPipelineJournal_nid_postfix();
extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix();
extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix();
extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class Driver {
std::pair<std::uint64_t, std::uint64_t> SerialProgress();
std::uint64_t FlipSubmissions() const;
std::vector<AwaitedWait> AwaitedWaits();
void WaitForPipelineJournal();
bool WaitIdleFor(std::chrono::milliseconds timeout);
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output);
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output);
Expand Down Expand Up @@ -469,7 +470,7 @@ class Driver {
std::map<std::uint32_t, QueueState> queues;
std::map<std::uint32_t, std::shared_ptr<IVideoOutput>> outputs;
struct EopFlip {
std::uint32_t handle;
FlipInfo info;
std::shared_ptr<IFlipRequest> request;
};
std::map<std::uint32_t, EopFlip> eopFlips;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ struct FlipInfo {
std::int64_t argument;
};

class EopFlipAlreadyReserved final : public std::runtime_error {
public:
EopFlipAlreadyReserved() : std::runtime_error("EOP flip token is already reserved for this flip") {}
};

class IFlipRequest {
public:
virtual ~IFlipRequest() = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ extern "C" std::size_t AgcDriverAwaitedWaits_nid_postfix(AgcDriver::AwaitedWait*
return count;
}

extern "C" void AgcDriverWaitForPipelineJournal_nid_postfix() {
AgcDriver::DriverDetail::Driver::Get().WaitForPipelineJournal();
}

extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std::uint64_t* completed) {
const auto [submitted, done] = AgcDriver::DriverDetail::Driver::Get().SerialProgress();
*accepted = submitted;
Expand Down
10 changes: 10 additions & 0 deletions core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ std::vector<AwaitedWait> Driver::AwaitedWaits() {
return result;
}

void Driver::WaitForPipelineJournal() {
std::shared_ptr<VulkanDevice> localDevice = device.Load();
if (localDevice == nullptr) {
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (device == nullptr) device = std::make_shared<VulkanDevice>();
localDevice = device;
}
localDevice->WaitForPipelineJournalReplay();
}

std::pair<std::uint64_t, std::uint64_t> Driver::SerialProgress() {
std::lock_guard lock(mutex);
return {accepted, completed};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ std::uint32_t Driver::ReserveEopFlipAt(const FlipInfo& info, std::uint32_t token
std::uint32_t Driver::reserveEopFlip(const FlipInfo& info, std::uint32_t token) {
rethrowFailure();
checkStopping();
require(token >= EopFlipTokenBase && token <= EopFlipTokenLimit && !eopFlips.contains(token), "EOP flip token is not free");
require(token >= EopFlipTokenBase && token <= EopFlipTokenLimit, "EOP flip token is out of range");
const auto output = outputs.find(info.handle);
require(output != outputs.end(), "EOP flip references an unregistered video output");
if (const auto found = eopFlips.find(token); found != eopFlips.end()) {
const auto& reserved = found->second.info;
require(reserved.handle == info.handle && reserved.index == info.index && reserved.mode == info.mode && reserved.argument == info.argument, "EOP flip token is reserved for a different flip");
throw EopFlipAlreadyReserved{};
}
auto request = output->second->TryReserve(info);
if (!request) return 0;
eopFlips.emplace(token, EopFlip{info.handle, std::move(request)});
eopFlips.emplace(token, EopFlip{info, std::move(request)});
nextEopToken = std::max(nextEopToken, token + 1);
return token;
}
Expand All @@ -53,7 +58,7 @@ void Driver::UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<I
std::lock_guard lock(mutex);
const auto it = outputs.find(handle);
require(it != outputs.end() && it->second == output, "video output registration mismatch");
std::erase_if(eopFlips, [handle](const auto& entry) { return entry.second.handle == handle; });
std::erase_if(eopFlips, [handle](const auto& entry) { return entry.second.info.handle == handle; });
outputs.erase(it);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ FrameTiming* Driver::includeTimingSubmission(const Submission& submission, bool
}

void Driver::markCompleted(std::uint64_t serial) {
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;
if (traceGpu) std::fprintf(stderr, "[gpu] %.1f completed serial=%llu (in order up to %llu)\n", TraceMs(), static_cast<unsigned long long>(serial), static_cast<unsigned long long>(completed));
completedOutOfOrder.insert(serial);
while (!completedOutOfOrder.empty() && *completedOutOfOrder.begin() == completed + 1) {
completed = *completedOutOfOrder.begin();
Expand All @@ -70,6 +72,8 @@ void Driver::deferCompletion(const Submission& submission) {
queue0Uncommitted.push_back(received);
}
completionDeferred() = true;
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;
if (traceGpu) std::fprintf(stderr, "[gpu] %.1f deferred completion serial=%llu to the draw pipeline\n", TraceMs(), static_cast<unsigned long long>(submission.serial));
DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites, progress = submission.progress, holdsFlip = submission.holdsFlip] {
completeSubmission(serial, received, labelWrites, progress, holdsFlip);
}, {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
}
submission.shaders = shaders;
submission.serial = accepted + 1;
if (trace) std::fprintf(stderr, "[gpu] %.1f accepted serial=%llu queue=0x%x dwords=%zu at %p%s\n", TraceMs(), static_cast<unsigned long long>(submission.serial), queue, submission.commands.size(),
static_cast<const void*>(descriptor.addr), cpuFlip ? " (cpu flip)" : "");

submission.received = ++eventSerial;
const auto now = std::chrono::steady_clock::now();
Expand Down Expand Up @@ -599,6 +601,8 @@ void Driver::SuspendPoint() {
Submission boundary{};
boundary.serial = accepted + 1;
boundary.suspend = true;
static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr;
if (trace) std::fprintf(stderr, "[gpu] %.1f accepted serial=%llu suspend point\n", TraceMs(), static_cast<unsigned long long>(boundary.serial));

boundary.queue = 0;
boundary.receivedAt = receivedAt;
Expand Down
3 changes: 2 additions & 1 deletion core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2415,7 +2415,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->oversizedBuffer = std::make_unique<Graphics::Buffer>(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
std::ranges::fill(state->oversizedBuffer->Bytes(), std::byte{0});
if (!compileOnly && Pm4::GdsImportable() && Graphics::HostImportFor(graphicsContext(), Pm4::GdsAddress(), Pm4::GdsBytes, Graphics::ImportRequester::Map) == nullptr) Pm4::RejectGdsImport();
state->pipelineCache = std::make_unique<Graphics::PipelineCache>(graphicsContext(), state->properties, window != nullptr || compileOnly);
static const bool headlessPipelineCache = std::getenv("APS5_HEADLESS_PIPELINE_CACHE") != nullptr;
state->pipelineCache = std::make_unique<Graphics::PipelineCache>(graphicsContext(), state->properties, window != nullptr || compileOnly || headlessPipelineCache);
state->detiler = std::make_unique<Graphics::TextureDetiler>(graphicsContext());
state->textureCache = std::make_unique<Graphics::TextureCache>(graphicsContext());
state->colorTransfer = std::make_unique<Graphics::GpuColorTransfer>(graphicsContext());
Expand Down
93 changes: 79 additions & 14 deletions core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "execution/VulkanTestDevice.hpp"
#include "prx/libSceAgc/DcbFlow/include/Memory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/FrameCapture.hpp"
Expand Down Expand Up @@ -134,7 +135,7 @@ bool equal(const std::byte* bytes, const std::vector<std::byte>& expected) {
return std::memcmp(bytes, expected.data(), expected.size()) == 0;
}

void testCaptureAndReplay(const std::filesystem::path& tool) {
void testCaptureAndReplay(const std::filesystem::path& tool, bool pipelines) {
const char* directory = std::getenv("APS5_FRAME_CAPTURE");
check(directory != nullptr && *directory != '\0', "APS5_FRAME_CAPTURE is not set");
const std::filesystem::path capture(directory);
Expand Down Expand Up @@ -186,17 +187,40 @@ void testCaptureAndReplay(const std::filesystem::path& tool) {
check(recorded(late.base), "the block committed during the capture is not in its ranges");
check(reader.Header().find("\"late_ranges\":1") != std::string::npos, "the capture header does not count one range committed during the capture: " + reader.Header());

const auto firstDump = capture / "first.bin";
const auto secondDump = capture / "second.bin";
auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + firstDump.string() +
"\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\"";
const auto firstDump = capture / "first dump.bin";
const auto secondDump = capture / "second dump.bin";
const auto replay = [&](const std::string& options, const std::filesystem::path& output) {
std::filesystem::remove(firstDump);
std::filesystem::remove(secondDump);
auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --strict " + options + " --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" +
firstDump.string() + "\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\" > \"" + output.string() + "\" 2> \"" + output.string() + ".log\"";
#ifdef _WIN32
command = "\"" + command + "\"";
command = "\"" + command + "\"";
#endif
const auto code = std::system(command.c_str());
check(code == 0, "agc_frame_replay exited with " + std::to_string(code));
check(readFile(firstDump) == first, "the replay did not reproduce the first copy");
check(readFile(secondDump) == second, "the replay did not reproduce the second copy");
const auto code = std::system(command.c_str());
check(code == 0, "agc_frame_replay " + options + " exited with " + std::to_string(code));
check(readFile(firstDump) == first, "the replay " + options + " did not reproduce the first copy");
check(readFile(secondDump) == second, "the replay " + options + " did not reproduce the second copy");
const auto json = readFile(output);
return std::string(reinterpret_cast<const char*>(json.data()), json.size());
};
replay("", capture / "replay output.json");
if (pipelines) {
check(std::getenv("APS5_HEADLESS_PIPELINE_CACHE") != nullptr, "the pipeline cache switch was not inherited");
const auto warm = capture / "warm output.json";
replay("", warm);
const auto warmLog = readFile(warm.string() + ".log");
check(std::string(reinterpret_cast<const char*>(warmLog.data()), warmLog.size()).find("[pipeline-cache] loaded ") != std::string::npos, "the warm replay did not load the seeded pipeline cache");
const auto cold = capture / "cold output.json";
replay("--cold-pipelines", cold);
const auto coldLog = readFile(cold.string() + ".log");
check(std::string(reinterpret_cast<const char*>(coldLog.data()), coldLog.size()).find("[pipeline-cache] loaded ") == std::string::npos, "--cold-pipelines loaded a cache with APS5_HEADLESS_PIPELINE_CACHE inherited");
return;
}
const auto repeated = replay("--repeat 3", capture / "repeat output.json");
std::size_t passes = 0;
for (auto found = repeated.find("\"frames\":2,\"p50\""); found != std::string::npos; found = repeated.find("\"frames\":2,\"p50\"", found + 1)) ++passes;
check(passes == 3, "the repeated replay did not replay both frames in each of three passes: " + repeated);
}

void testConcurrentCopies(const std::filesystem::path& tool) {
Expand Down Expand Up @@ -261,16 +285,57 @@ void testConcurrentCopies(const std::filesystem::path& tool) {
}
}

void testReservationError(const std::filesystem::path& tool) {
const char* directory = std::getenv("APS5_FRAME_CAPTURE");
check(directory != nullptr && *directory != '\0', "APS5_FRAME_CAPTURE is not set");
const std::filesystem::path capture(directory);
std::uintptr_t base = 0;
std::size_t size = 0;
GuestArena::GuestArenaRange_nid_postfix(&base, &size);
AgcDriver::FrameCaptureWriter writer(capture);
AgcDriver::FrameCall output;
output.kind = FrameCallKind::RegisterVideoOutput;
output.values = {Handle};
writer.WriteCall(output);
AgcDriver::FrameCall reservation;
reservation.kind = FrameCallKind::ReserveEopFlip;
reservation.values = {Handle, static_cast<std::uint64_t>(-1), 1, 0, AgcDriver::EopFlipTokenBase};
writer.WriteCall(reservation);
AgcDriver::FrameCall start;
start.kind = FrameCallKind::FrameStart;
writer.WriteCall(start);
output.kind = FrameCallKind::UnregisterVideoOutput;
writer.WriteCall(output);
output.kind = FrameCallKind::RegisterVideoOutput;
writer.WriteCall(output);
reservation.values[3] = 1;
writer.WriteCall(reservation);
writer.Finish("{\"arena_base\":" + std::to_string(base) + "}");
const auto result = capture / "reservation error.json";
const auto log = capture / "reservation error.log";
auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --repeat 2 --strict > \"" + result.string() + "\" 2> \"" + log.string() + "\"";
#ifdef _WIN32
command = "\"" + command + "\"";
#endif
check(std::system(command.c_str()) != 0, "the replay accepted an incompatible reservation on the second pass");
const auto errors = readFile(log);
check(std::string(reinterpret_cast<const char*>(errors.data()), errors.size()).find("EOP flip token is reserved for a different flip") != std::string::npos, "the repeated replay did not report the incompatible reservation");
const auto report = readFile(result);
check(std::string(reinterpret_cast<const char*>(report.data()), report.size()).find("\"p50\"") == std::string::npos, "a failed repeated reservation produced a successful replay report");
}

}

int main(int argc, char** argv) {
if (argc != 2 && !(argc == 3 && std::string(argv[2]) == "concurrent")) {
std::fprintf(stderr, "usage: agc_driver_frame_replay_tests <agc_frame_replay> [concurrent]\n");
if (argc != 2 && !(argc == 3 && (std::string(argv[2]) == "concurrent" || std::string(argv[2]) == "pipelines" || std::string(argv[2]) == "reservation_error"))) {
std::fprintf(stderr, "usage: agc_driver_frame_replay_tests <agc_frame_replay> [concurrent|pipelines|reservation_error]\n");
return 2;
}
try {
if (argc == 3) testConcurrentCopies(argv[1]);
else testCaptureAndReplay(argv[1]);
if (!OpenVulkanTestDevice()) return VulkanTestSkipped;
if (argc == 3 && std::string(argv[2]) == "concurrent") testConcurrentCopies(argv[1]);
else if (argc == 3 && std::string(argv[2]) == "reservation_error") testReservationError(argv[1]);
else testCaptureAndReplay(argv[1], argc == 3);
} catch (const std::exception& error) {
std::fprintf(stderr, "FAILED: %s\n", error.what());
return 1;
Expand Down
Loading
Loading