Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/Submit.cpp
T
Dean Galvin a3ffb73309 perf(agc): deliver end-of-pipe interrupts after their work without draining
A RELEASE_MEM with an interrupt select drained the device: the queue
worker submitted everything recorded and waited for the GPU before it
stored the label and delivered the interrupt. PPSA21564 issues about 300
of them per frame, so the worker and the GPU took turns.

The label is now recorded like any other (on the GPU behind the work, or
as a completion action behind pending write-backs), and the delivery
becomes a completion action of the batch holding the work before the
packet (Recorder::AfterRecordedWork, after the batch's earlier
completions). It is counted with the completion labels, so idle workers
and pollers reap it, and it starts the label flush deadline. With draws
or dispatches recorded since the last submit the batch is submitted at
once. A batch holding only label stores stays open and goes out with the
next work, at the label flush deadline, at a wait, at a flip or at the
end of the submission, like any other pending label. A label the GPU
cannot store, or one that does not decode, still drains.

The interrupt so never arrives before the work ahead of it completed and
its label landed; it can only arrive later, by at most the flush
deadline plus the work recorded into the same batch.
2026-10-03 09:56:05 +13:00

338 lines
17 KiB
C++

#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libSceAgcDriver/Eq/include/Query.hpp"
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name);
extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq);
static_assert(sizeof(Packet) == 16);
static_assert(offsetof(Packet, addr) == 0);
static_assert(offsetof(Packet, dw_num) == 8);
static_assert(offsetof(Packet, flags) == 12);
namespace {
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template<typename TAction>
std::string expectFailure(TAction action) {
try {
action();
} catch (const std::runtime_error& error) {
return error.what();
}
throw std::runtime_error("expected exception");
}
void testEvents() {
KernelEvent event{};
event.filter = -14;
event.ident = 0x40;
event.data = 123;
check(sceAgcDriverGetEqEventType(&event) == 0x40, "graphics event uses wrong field");
event.filter = -1;
event.data = -17;
check(sceAgcDriverGetEqEventType(&event) == -17, "non-graphics event uses wrong field");
event.data = std::numeric_limits<std::intptr_t>::max();
expectFailure([&] { sceAgcDriverGetEqEventType(&event); });
event.filter = -14;
event.ident = std::numeric_limits<std::uintptr_t>::max();
expectFailure([&] { sceAgcDriverGetEqEventType(&event); });
expectFailure([] { sceAgcDriverGetEqEventType(nullptr); });
expectFailure([&] { sceAgcDriverGetEqEventType(reinterpret_cast<const KernelEvent*>(reinterpret_cast<const std::byte*>(&event) + 1)); });
}
void testValidation() {
std::array<std::uint32_t, 3> commands{0xc0017600, 0x20c, 0};
Packet packet{commands.data(), 3, 0, {}};
expectFailure([] { sceAgcDriverSubmitDcb(nullptr); });
expectFailure([] { sceAgcDriverAgrSubmitDcb(nullptr); });
expectFailure([] { sceAgcDriverSubmitAcb(0x20, nullptr); });
expectFailure([&] { sceAgcDriverSubmitAcb(0, &packet); });
expectFailure([&] { sceAgcDriverSubmitAcb(0x58, &packet); });
packet.dw_num = 2;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
packet.dw_num = 3;
packet.flags = 1;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
packet.flags = 0;
commands[0] = 0xc001ff00;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
commands[0] = 0xc001105c;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
commands[0] = 0xc0017601;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
commands[0] = 0xc0017600;
commands[1] = 0x10000;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
packet.addr = reinterpret_cast<std::uint32_t*>(reinterpret_cast<std::uintptr_t>(commands.data()) + 1);
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
packet.addr = reinterpret_cast<std::uint32_t*>(std::numeric_limits<std::uintptr_t>::max() - 3);
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
packet.addr = reinterpret_cast<std::uint32_t*>(0x1000);
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
AgcDriverWaitIdle_nid_postfix();
}
void testClearState() {
AgcDriver::QueueState graphics{{{0x20c, 1}}, {{0x10, 17}, {0x11, 23}}, {{0x242, 5}}};
const auto shader = graphics.shader;
const auto userConfig = graphics.userConfig;
graphics.ClearContext();
check(graphics.context == AgcDriver::InitialContextRegisters(), "CLEAR_STATE retained context registers");
check(graphics.shader == shader && graphics.userConfig == userConfig, "CLEAR_STATE reset unrelated registers");
graphics.context.emplace(0x10, 31);
graphics.ClearContext();
check(graphics.context == AgcDriver::InitialContextRegisters(), "repeated CLEAR_STATE retained context registers");
std::array<std::uint32_t, 3> words{0xc0001200, 0, 0};
Packet packet{words.data(), 2, 0, {}};
expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); });
words[1] = 0x10;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
words[1] = 0;
words[0] = 0xc0011200;
packet.dw_num = 3;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
words[0] = 0xc0001201;
packet.dw_num = 2;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
words[0] = 0xc0001200;
packet.dw_num = 1;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
packet.dw_num = 2;
for (std::uint32_t state = 0; state <= 0xf; ++state) {
words[1] = state;
check(sceAgcDriverSubmitDcb(&packet) == 0, "CLEAR_STATE submit failed");
}
AgcDriverWaitIdle_nid_postfix();
}
void testSubmissions() {
std::vector<std::thread> producers;
std::array<std::exception_ptr, 4> errors{};
for (std::uint32_t i = 0; i < errors.size(); ++i) {
producers.emplace_back([&, i] {
try {
for (std::uint32_t j = 0; j < 100; ++j) {
std::array<std::uint32_t, 5> words{0xc0017600, 0x240, j, 0xc0001000, 0};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
if (i == 0) check(sceAgcDriverSubmitDcb(&packet) == 0, "DCB submit failed");
else if (i == 1) check(sceAgcDriverAgrSubmitDcb(&packet) == 0, "AGR submit failed");
else check(sceAgcDriverSubmitAcb(i == 2 ? 0x20 : 0x57, &packet) == 0, "ACB submit failed");
words.fill(0xffffffffu);
}
} catch (...) {
errors[i] = std::current_exception();
}
});
}
for (auto& producer : producers) producer.join();
for (auto& error : errors) if (error) std::rethrow_exception(error);
AgcDriverWaitIdle_nid_postfix();
Packet empty{};
check(sceAgcDriverSubmitDcb(&empty) == 0, "empty submit failed");
AgcDriverWaitIdle_nid_postfix();
}
void testEndOfPipeInterrupts() {
KernelEqueue eq = 0;
check(sceKernelCreateEqueue(&eq, "AGC test") == 0, "event queue creation failed");
auto owner = EqueuePin_nid_postfix(eq);
int graphicsTag = 0;
int computeTag = 0;
check(sceAgcDriverAddEqEvent(eq, 0, &graphicsTag) == 0, "graphics event registration failed");
check(sceAgcDriverAddEqEvent(eq, 0x20, &computeTag) == 0, "compute event registration failed");
expectFailure([] { sceAgcDriverAddEqEvent(0, 0, nullptr); });
std::array<std::uint32_t, 8> words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0 && sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit failed");
AgcDriverWaitIdle_nid_postfix();
std::array<KernelEvent, 2> events{};
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "graphics end-of-pipe interrupt was not delivered to its queue only");
check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 2 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "delivered interrupt was not cleared");
check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute interrupt submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &computeTag && sceAgcDriverGetEqEventType(events.data()) == 0x20, "compute end-of-pipe interrupt missing");
words[2] = 0;
check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt");
check(sceAgcDriverDeleteEqEvent(eq, 0) == 0, "graphics event deletion failed");
expectFailure([&] { sceAgcDriverDeleteEqEvent(eq, 0); });
words[2] = 1u << 24u;
check(sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit after deletion failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "deleted event still received interrupts");
check(sceAgcDriverDeleteEqEvent(eq, 0x20) == 0, "compute event deletion failed");
owner.reset();
check(sceKernelDeleteEqueue(eq) == 0, "event queue deletion failed");
}
std::array<std::uint32_t, 5> writeData(volatile std::uint32_t* address, std::uint32_t value) {
const auto target = reinterpret_cast<std::uintptr_t>(address);
return {0xc0033700, 0x00100200, static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(static_cast<std::uint64_t>(target) >> 32u), value};
}
std::array<std::uint32_t, 7> waitEqual(volatile std::uint32_t* address, std::uint32_t value) {
const auto target = reinterpret_cast<std::uintptr_t>(address);
return {0xc0053c00, 0x13, static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(static_cast<std::uint64_t>(target) >> 32u), value, 0xffffffffu, 0x19};
}
std::array<std::uint32_t, 9> waitEqual64(volatile std::uint32_t* address, std::uint64_t value, std::uint64_t mask) {
const auto target = reinterpret_cast<std::uintptr_t>(address);
return {0xc0079300, 0x13, static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(static_cast<std::uint64_t>(target) >> 32u), static_cast<std::uint32_t>(value), static_cast<std::uint32_t>(value >> 32u), static_cast<std::uint32_t>(mask), static_cast<std::uint32_t>(mask >> 32u), 0x19};
}
void submit(std::uint32_t queue, const std::vector<std::uint32_t>& words) {
Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}};
check((queue == 0 ? sceAgcDriverSubmitDcb(&packet) : sceAgcDriverSubmitAcb(queue, &packet)) == 0, "label submit failed");
}
std::chrono::milliseconds waitFor(volatile std::uint32_t* address, std::uint32_t value, const char* message) {
const auto start = std::chrono::steady_clock::now();
while (*address != value) {
check(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), message);
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
}
template<std::size_t... N>
std::vector<std::uint32_t> commands(const std::array<std::uint32_t, N>&... packets) {
std::vector<std::uint32_t> words;
(words.insert(words.end(), packets.begin(), packets.end()), ...);
return words;
}
std::array<std::uint32_t, 8> endOfPipeLabel(volatile std::uint32_t* address, std::uint32_t value) {
const auto target = reinterpret_cast<std::uintptr_t>(address);
return {0xc0064900, 0x514, (1u << 29u) | (2u << 24u), static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(static_cast<std::uint64_t>(target) >> 32u), value, 0, 0};
}
void testEndOfPipeLabelsWithoutWork() {
alignas(64) static volatile std::uint32_t first = 0, second = 0, done = 0;
KernelEqueue eq = 0;
check(sceKernelCreateEqueue(&eq, "AGC EOP labels") == 0, "event queue creation failed");
auto owner = EqueuePin_nid_postfix(eq);
int tag = 0;
check(sceAgcDriverAddEqEvent(eq, 0, &tag) == 0, "graphics event registration failed");
submit(0x20, commands(waitEqual(&second, 2), writeData(&done, 1)));
submit(0, commands(endOfPipeLabel(&first, 1), endOfPipeLabel(&second, 2)));
waitFor(&done, 1, "an end-of-pipe label with no work before it never landed");
check(first == 1 && second == 2, "end-of-pipe labels with no work before them landed wrong");
AgcDriverWaitIdle_nid_postfix();
std::array<KernelEvent, 2> events{};
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &tag && events[0].data == 2, "end-of-pipe labels with no work before them lost their interrupts");
check(sceAgcDriverDeleteEqEvent(eq, 0) == 0, "graphics event deletion failed");
owner.reset();
check(sceKernelDeleteEqueue(eq) == 0, "event queue deletion failed");
}
void testLabelStoredSinceSubmission() {
alignas(64) static volatile std::uint32_t gate = 0, label = 0, done = 0, late = 0;
submit(0x20, commands(waitEqual(&gate, 1), waitEqual(&label, 1), writeData(&done, 1)));
submit(0, commands(writeData(&label, 1)));
waitFor(&label, 1, "producer label never landed");
label = 0;
gate = 1;
check(waitFor(&done, 1, "consumer never passed its waits") < std::chrono::milliseconds(500), "a label stored after the wait's submission did not satisfy it");
AgcDriverWaitIdle_nid_postfix();
submit(0x20, commands(waitEqual(&label, 1), writeData(&late, 1)));
check(waitFor(&late, 1, "consumer never passed its wait") >= std::chrono::milliseconds(900), "a label stored before the wait's submission satisfied it");
AgcDriverWaitIdle_nid_postfix();
}
void testWideLabelStoredSinceSubmission() {
alignas(64) static volatile std::uint32_t gate = 0, done = 0, late = 0;
alignas(64) static volatile std::uint32_t label[2] = {0, 0x5eed};
submit(0x20, commands(waitEqual(&gate, 1), waitEqual64(label, 1, 0xffffffffu), writeData(&done, 1)));
submit(0, commands(writeData(label, 1)));
waitFor(label, 1, "producer label never landed");
label[0] = 0;
gate = 1;
check(waitFor(&done, 1, "consumer never passed its waits") < std::chrono::milliseconds(500), "a 32-bit label stored after a low-dword 64-bit wait's submission did not satisfy it");
AgcDriverWaitIdle_nid_postfix();
gate = 0;
submit(0x20, commands(waitEqual(&gate, 1), waitEqual64(label, 1, ~0ull), writeData(&late, 1)));
submit(0, commands(writeData(label, 1)));
waitFor(label, 1, "producer label never landed");
label[0] = 0;
gate = 1;
check(waitFor(&late, 1, "consumer never passed its wait") >= std::chrono::milliseconds(900), "a 32-bit store satisfied a 64-bit wait whose high dword never matched");
AgcDriverWaitIdle_nid_postfix();
}
void testLabelHeldAtSubmission() {
alignas(64) static volatile std::uint32_t gate = 0, label = 1, done = 0, reset = 0;
submit(0x20, commands(waitEqual(&gate, 1), waitEqual(&label, 1), writeData(&done, 1)));
label = 0;
gate = 1;
check(waitFor(&done, 1, "consumer never passed its waits") < std::chrono::milliseconds(500), "a label held when the wait was submitted did not satisfy it");
AgcDriverWaitIdle_nid_postfix();
gate = 0;
label = 1;
submit(0x20, commands(waitEqual(&gate, 1), writeData(&label, 0), waitEqual(&label, 1), writeData(&reset, 1)));
gate = 1;
check(waitFor(&reset, 1, "consumer never passed its wait") >= std::chrono::milliseconds(900), "a label its own queue stored first counted as held at the submission");
AgcDriverWaitIdle_nid_postfix();
}
void testWorkerFailure() {
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
check(sceAgcDriverSubmitAcb(0x21, &packet) == 0, "dispatch was not accepted");
std::array<std::string, 4> messages;
std::vector<std::thread> waiters;
for (auto& message : messages) {
waiters.emplace_back([&message] { message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }); });
}
for (auto& waiter : waiters) waiter.join();
for (const auto& message : messages) check(message.find("required shader register") != std::string::npos, "worker failure was lost");
check(expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }) == messages[0], "subsequent DCB lost worker failure");
check(expectFailure([&] { sceAgcDriverAgrSubmitDcb(&packet); }) == messages[0], "subsequent AGR lost worker failure");
check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }) == messages[0], "subsequent ACB lost worker failure");
}
}
int main() {
try {
testEvents();
testValidation();
testClearState();
testSubmissions();
testEndOfPipeInterrupts();
testLabelStoredSinceSubmission();
testEndOfPipeLabelsWithoutWork();
testLabelHeldAtSubmission();
testWideLabelStoredSinceSubmission();
testWorkerFailure();
check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure");
std::puts("AGC driver submit tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { LibcRunShutdown_nid_postfix(); }
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
return 1;
}
}