Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
T
fdfawef324andClaude Opus 5.5 13b171cab5 feat(agc): keep the last submissions per queue and dump them on a stall or crash
A GPU hang or a crash in a title leaves no record of what the queues
were last given: APS5_TRACE_GPU prints every submission (too much to
leave on), and the WAIT_REG_MEM timeout message shows only the packet
before the wait on the waiting queue.

Each queue now keeps a ring of its last 16 submissions
(APS5_PM4_RING=<n>, 0 to 4096, 0 disables): the TraceMs time in tenths
of a millisecond, serial, size, guest address and a copy of the first
512 dwords after indirect buffers are inlined, recorded under the
driver lock next to the serial, REWIND tails included. The ring of a
queue and the failure report registration are set up at the top of
Submit, before any output is reserved, so recording under the lock
does not allocate. Disabled, Submit only tests a static flag; enabled,
it costs one bounded copy per submission.

DumpSubmissionRings decodes the kept dwords with Pm4::PacketWords and
the new Pm4::KnownName, which returns the decoder's names as views of
static strings (empty for an unknown opcode, printed as UNKNOWN_0x%02x);
Pm4::Name now builds its std::string from it. Every line is formatted
with integer and string conversions only into a 512-byte stack buffer
and written with one WriteFile / write(2) on the stderr handle, so the
dump does not allocate from the C runtime heap, lines from other
threads do not interleave and the CRT stream lock is not taken (the
rule of #3871). The header gives the number of queues and the ring
size, and each queue line the submissions recorded and kept.

libc gets a small registry of failure reports
(LibcRegisterFailureReport_nid_no_patch, Unregister, Run); Run runs
the registered reports the first time it is called and returns at once
after that. The Windows crash report runs it after the thread dump of
an unhandled exception, after SIGABRT (which also covers the
APS5_HANG_DUMP_SECS timed dump, since it aborts) and after an invalid
CRT parameter, so an abort after a reported exception does not print
the rings twice. The driver registers its dump at the first submission
and unregisters at static destruction; the dump there waits a bounded
time for each ring lock. What the crash-path dump cannot do (a ring
held by the crashed thread, rings corrupted by the crash, Linux) is
recorded in technical debt.

APS5_STALL_WATCHDOG_SECS=<n> (0 to 86400, 0 disables) makes a waiter
that has not progressed for n seconds dump the rings once per wait: a
WAIT_REG_MEM (progress as for APS5_GPU_WAIT_TIMEOUT_MS: another packet
runs or completes), a REWIND waiting for the title, and a compute
submission held behind queue 0. The last one prints under the driver
lock, so submissions and the other queues wait for it (USAGE.md says
so). Both variables are read at the top of Submit, so an invalid value
of either throws at the first submission and never later inside a GPU
worker. The order wait behind queue 0 releases its order hold on scope
exit, so a throwing dump cannot leave it counted.

No packet semantics change; packet sizes and names are those of the
existing decoder (PM4 type-3 header layout as in Mesa
src/amd/common/sid.h). The queue bound of Submit and of the rings is
one constant, SubmissionQueueEnd.

Tests: agc_driver_pm4_tests gets ten modes, each its own ctest:
agc_driver_pm4_ring (APS5_PM4_RING=4, six submissions on queue 0 and
two on 0x20: the dump holds #3..#6 and #1..#2 in order, nothing
older), agc_driver_pm4_ring_empty (one line), _ring_disabled (one
line), _ring_invalid and agc_driver_pm4_stall_watchdog_invalid
(APS5_STALL_WATCHDOG_SECS=1x: the submission throws and is not
executed), _ring_report (LibcRunFailureReports_nid_no_patch dumps the
rings), _ring_report_heap (Windows: with ucrtbase.dll's HeapAlloc and
HeapReAlloc imports counting calls from the reporting thread, the
failure report of a submission holding INDEX_BUFFER_SIZE names it and
allocates nothing, while Pm4::Name of the same header is seen
allocating), and with APS5_STALL_WATCHDOG_SECS=1 a WAIT_REG_MEM
(agc_driver_pm4_stall_watchdog), a REWIND (_stall_watchdog_rewind) and
a compute submission held behind a queue 0 rendering wait
(_stall_watchdog_order), each held for about 2.5 s, dump once and
complete when released. The PM4 catalog test checks KnownName against
every opcode and custom name. windows_backtrace_failure_report checks
that abort() runs the registered reports once and not an unregistered
one; windows_backtrace_failure_report_once that abort() does not run
them again after an earlier run. With the dump decoding through
Pm4::Name, agc_driver_pm4_ring_report_heap fails with "the failure
report allocated from the C runtime heap"; with Run resetting its
flag, windows_backtrace_failure_report_once prints the report twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:18:26 +02:00

1716 lines
107 KiB
C++

#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libc/include/FailureReport.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <memory>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#endif
extern "C" int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size);
namespace {
void check(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
template<typename TAction>
void expectFailure(TAction action, const char* text) {
try { action(); }
catch (const std::runtime_error& error) {
check(std::string(error.what()).find(text) != std::string::npos, error.what());
return;
}
throw std::runtime_error("expected PM4 rejection");
}
std::vector<std::uint32_t> makePacket(std::uint32_t opcode, std::initializer_list<std::uint32_t> payload, std::uint32_t flags = 0) {
std::vector<std::uint32_t> result{0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u) | flags};
result.insert(result.end(), payload);
return result;
}
std::uint32_t low(const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer)); }
std::uint32_t high(const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); }
void execute(AgcDriver::QueueState& state, const std::vector<std::uint32_t>& packet) {
AgcDriver::DriverDetail::RegisterStateKey(state);
AgcDriver::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Execute(packet, state);
check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint");
}
void testCatalog() {
std::set<std::uint32_t> values;
for (const auto& opcode : AgcDriver::Pm4::Opcodes) {
check(values.insert(opcode.value).second, "duplicate PM4 opcode");
const auto packet = makePacket(opcode.value, {0});
check(AgcDriver::Pm4::Name(packet[0]) == opcode.name && AgcDriver::Pm4::KnownName(packet[0]) == opcode.name, "opcode name mismatch");
const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]);
if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str());
}
check(values.size() == 56, "reference opcode catalog is incomplete");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known");
check(AgcDriver::Pm4::KnownName(makePacket(0xff, {0})[0]).empty() && AgcDriver::Pm4::Name(makePacket(0xff, {0})[0]) == "UNKNOWN_0xff", "an unknown opcode is named");
const std::array<std::pair<std::uint32_t, const char*>, 11> custom{{
{5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"},
{12, "POP_MARKER"}, {20, "ACQUIRE_MEM_CUSTOM"}, {21, "WRITE_DATA_CUSTOM"}, {23, "FLIP"},
{24, "RELEASE_MEM_CUSTOM"}, {25, "DMA_DATA_CUSTOM"}, {26, "CONTEXT_STATE"}
}};
for (const auto& [id, name] : custom) {
const auto header = makePacket(0x10, {0}, id << 2)[0];
check(AgcDriver::Pm4::Name(header) == name && AgcDriver::Pm4::KnownName(header) == name, "custom opcode name mismatch");
}
}
void testNopPad() {
check(AgcDriver::Pm4::PacketWords(0xffff1000u) == 1, "the one-dword NOP is not one dword");
for (const auto queue : {0u, 0x20u}) AgcDriver::Pm4::Validate(std::vector<std::uint32_t>{0xffff1000u}, queue);
expectFailure([] { AgcDriver::Pm4::Validate(std::vector<std::uint32_t>{0xffff1000u, 0}, 0); }, "filler size");
for (const auto header : {0xffff1001u, 0xffff1002u}) check(AgcDriver::Pm4::PacketWords(header) == 0x4001, "a NOP with count 0x3fff and header flags is not sized by its count");
AgcDriver::Pm4::Validate(makePacket(0x10, {0}), 0);
std::vector<std::uint32_t> largest(0x4000, 0);
largest[0] = 0xfffe1000u;
check(AgcDriver::Pm4::PacketWords(largest[0]) == largest.size(), "the largest counted NOP is not sized by its count");
AgcDriver::Pm4::Validate(largest, 0);
}
void testRegisters() {
AgcDriver::QueueState state;
execute(state, makePacket(0x79, {0x242, 4}));
check(state.userConfig.at(0x242) == 4, "primitive type register write was lost");
execute(state, makePacket(0x79, {0x242, 6}));
check(state.userConfig.at(0x242) == 6, "primitive type register update was lost");
const std::array<std::uint32_t, 3> indirectOpcodes{0x9f, 0x63, 0x64};
for (auto opcode : indirectOpcodes) {
std::array<std::uint32_t, 6> pairs{0x10, 41, 0x11, 42, 0x10, 43};
auto packet = makePacket(opcode, {low(pairs.data()), high(pairs.data()), 0x80000000, 3});
execute(state, packet);
const auto& registers = opcode == 0x9f ? state.context : opcode == 0x63 ? state.shader : state.userConfig;
check(registers.at(0x10) == 43 && registers.at(0x11) == 42, "indirect register order or bank lost");
pairs[0] = 0x12;
pairs[4] = 0xffffffffu;
expectFailure([&] { execute(state, packet); }, "sentinel");
check(!registers.contains(0x12), "invalid indirect packet partially changed state");
std::array<std::uint32_t, 2> values{0x11223344, 0x55667788};
execute(state, makePacket(opcode, {low(values.data()), high(values.data()), 0x0a, 2}));
check(registers.at(0x0a) == 0x11223344 && registers.at(0x0b) == 0x55667788, "consecutive indirect registers lost");
expectFailure([&] { execute(state, makePacket(opcode, {low(values.data()), high(values.data()), 0xffff, 2})); }, "indirect-register");
expectFailure([&] { execute(state, makePacket(opcode, {low(values.data()), high(values.data()), 0x4000000a, 2})); }, "indirect-register");
packet[1] = 0x1000;
packet[2] = 0;
expectFailure([&] { execute(state, packet); }, "guest");
packet[3] = 0x10000;
expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, "control");
}
execute(state, makePacket(0x69, {0x11, 50, 51}));
check(state.context.at(0x11) == 50 && state.context.at(0x12) == 51, "direct registers not sequential");
execute(state, makePacket(0x7a, {0x10, 60}));
check(state.userConfig.at(0x10) == 60, "uconfig index zero failed");
execute(state, makePacket(0x7a, {0x20000243, 0x441}));
check(state.indexType == 1 && state.userConfig.at(0x243) == 0x441, "indexed VGT_INDEX_TYPE write lost state");
execute(state, makePacket(0x7a, {0x10000242, 0x11}));
check(state.userConfig.at(0x242) == 0x11, "indexed VGT_PRIMITIVE_TYPE write lost state");
expectFailure([&] { execute(state, makePacket(0x7a, {0x10000010, 1})); }, "bank selection");
expectFailure([&] { execute(state, makePacket(0x7a, {0x10000243, 1})); }, "bank selection");
expectFailure([&] { execute(state, makePacket(0x7a, {0x20000242, 1})); }, "bank selection");
execute(state, makePacket(0x28, {0x80000000, 0x80000000}));
execute(state, makePacket(0x28, {0, 0}));
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000001, 0x80000000}), 0); }, "shadowing");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000000, 0x80000002}), 0); }, "shadowing");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000000, 0x80000000}), 0x20); }, "compute queue");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x28, {0x80000000}), 0); }, "packet size");
expectFailure([&] { execute(state, makePacket(0x69, {0xffff, 1, 2})); }, "overflow");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x9f, {0, 0, 0x80000000, 0}), 0x20); }, "compute");
}
void testRegisterFile() {
AgcDriver::Registers registers{{0x300, 3}, {0x10, 1}, {0x41, 2}};
check(registers.size() == 3 && !registers.contains(0x11) && registers.at(0x41) == 2, "register file lookup");
check(registers.find(0x12) == registers.end() && registers.find(0x10)->second == 1, "register file find");
check(!registers.emplace(0x10, 9).second && registers.at(0x10) == 1, "register file emplace replaced a value");
check(registers.insert_or_assign(0x10, 7).second == false && registers.at(0x10) == 7, "register file assignment");
check(registers.lower_bound(0x11)->first == 0x41 && registers.upper_bound(0x41)->first == 0x300 && registers.lower_bound(0x301) == registers.end(), "register file bounds");
std::vector<std::pair<std::uint32_t, std::uint32_t>> order;
for (const auto& [offset, value] : registers) order.emplace_back(offset, value);
check(order == std::vector<std::pair<std::uint32_t, std::uint32_t>>{{0x10, 7}, {0x41, 2}, {0x300, 3}}, "register file order");
std::vector<std::pair<std::uint32_t, std::uint32_t>> visited;
registers.ForEach([&](std::uint32_t offset, std::uint32_t value) { visited.emplace_back(offset, value); });
check(visited == order, "register file visit order");
auto copy = registers;
copy[0x7000] = 5;
check(copy.size() == 4 && registers.size() == 3 && !registers.contains(0x7000) && !(copy == registers), "register file copy");
check(copy.erase(0x7000) == 1 && copy.erase(0x7000) == 0 && copy == registers, "register file erase");
visited.clear();
copy.ForEach([&](std::uint32_t offset, std::uint32_t value) { visited.emplace_back(offset, value); });
check(visited == order, "register file visited an erased register");
bool threw = false;
try {
static_cast<void>(registers.at(0x42));
} catch (const std::out_of_range&) {
threw = true;
}
check(threw, "register file read an unset register");
}
void testContextAndBases() {
AgcDriver::QueueState state;
for (const auto offset : {0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu}));
execute(state, makePacket(0x12, {0}));
for (const auto offset : {0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore stencil defaults");
execute(state, makePacket(0x69, {0x10, 17}));
execute(state, makePacket(0x76, {0x20c, 2}));
execute(state, makePacket(0x10, {3, 0}, 0x68));
check(state.context == AgcDriver::InitialContextRegisters() && state.shader.at(0x20c) == 2, "push-clear reset wrong state");
expectFailure([&] { execute(state, makePacket(0x10, {1, 0}, 0x68)); }, "already pushed");
execute(state, makePacket(0x69, {0x10, 19}));
execute(state, makePacket(0x10, {2, 0}, 0x68));
check(state.context.at(0x10) == 17, "pop did not restore context");
expectFailure([&] { execute(state, makePacket(0x10, {2, 0}, 0x68)); }, "not been pushed");
alignas(8) std::array<std::uint32_t, 4> arguments{7, 8, 9, 0};
execute(state, makePacket(0x11, {1, low(arguments.data()), high(arguments.data())}, 2));
auto packet = makePacket(0x16, {0, 0x8041});
AgcDriver::Pm4::Validate(packet, 0);
auto resolved = AgcDriver::Pm4::ResolveDispatch(packet, state);
check(resolved == std::array<std::uint32_t, 5>{0xc0031500, 7, 8, 9, 0x8041}, "base-relative dispatch arguments changed");
packet = makePacket(0x16, {low(arguments.data()), high(arguments.data()), 0x41});
AgcDriver::Pm4::Validate(packet, 0x20);
check(AgcDriver::Pm4::ResolveDispatch(packet, state)[3] == 9, "absolute indirect dispatch arguments changed");
AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x2041}), 0);
AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0xa041}), 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x4041}), 0); }, "dispatch modifiers");
AgcDriver::Pm4::Validate(makePacket(0x15, {0x280, 1, 1, 0x2045}, 2), 0);
AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x2045}, 2), 0);
AgcDriver::Pm4::Validate(makePacket(0x15, {0xc0, 1, 1, 0x2047}, 2), 0);
AgcDriver::Pm4::Validate(makePacket(0x15, {0xc0, 1, 1, 0x2007}, 2), 0);
AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x2005}, 2), 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x40}), 0); }, "dispatch modifiers");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x2047}, 2), 0); }, "indirect dispatch modifiers");
using Threads = std::array<std::uint32_t, 3>;
check(AgcDriver::Pm4::PartialGroupThreads({0xc0, 1, 1}, {0x00400040, 0x00010001, 0x00010001}) == Threads{}, "complete last groups reported partial threads");
check(AgcDriver::Pm4::PartialGroupThreads({3, 2, 1}, {0x00100040, 0x00080008, 0x00010001}) == Threads{144, 16, 1}, "partial last group threads lost");
check(AgcDriver::Pm4::PartialGroupThreads({0, 1, 1}, {0x00100040, 0x00010001, 0x00010001}) == Threads{}, "an empty dispatch reported partial threads");
expectFailure([] { static_cast<void>(AgcDriver::Pm4::PartialGroupThreads({1, 1, 1}, {0x00000040, 0x00010001, 0x00010001})); }, "partial group size");
expectFailure([] { static_cast<void>(AgcDriver::Pm4::PartialGroupThreads({1, 1, 1}, {0x00410040, 0x00010001, 0x00010001})); }, "partial group size");
expectFailure([] { static_cast<void>(AgcDriver::Pm4::PartialGroupThreads({1, 0, 1}, {0x00100040, 0x00010001, 0x00000001})); }, "partial group size");
expectFailure([] { static_cast<void>(AgcDriver::Pm4::PartialGroupThreads({0x10002, 1, 1}, {0x0001ffff, 0x00010001, 0x00010001})); }, "exceeds 32 bits");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x2063}, 2), 0); }, "dispatch modifiers");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x41}, 4), 0); }, "header flags");
AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x49}), 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x51}), 0); }, "dispatch modifiers");
execute(state, makePacket(0x13, {32}));
execute(state, makePacket(0x26, {0x1000, 1}));
execute(state, makePacket(0x2a, {1}));
execute(state, makePacket(0x2f, {3}));
check(state.indexBufferSize == 32 && state.indexBase == 0x100001000ull && state.indexType == 1 && state.instanceCount == 3, "draw setup state lost");
execute(state, makePacket(0x10, {0x00636261}, 0x2c));
check(state.markers.back() == "abc", "marker text lost");
execute(state, makePacket(0x10, {0}, 0x30));
execute(state, makePacket(0x10, {0}, 0x30));
check(state.markers.empty(), "an unbalanced marker pop changed the marker stack");
execute(state, makePacket(0x10, {0}, 0x24));
check(state.shader.empty() && state.context == AgcDriver::InitialContextRegisters() && state.dispatchIndirectBase == 0 && state.indexBase == 0 && !state.savedContext, "dispatch reset retained state");
}
void testAutoDraw() {
check(AgcDriver::Pm4::AccessesMemory(0xc0012d00u), "auto draw must synchronize guest memory");
AgcDriver::QueueState state;
state.instanceCount = 4;
state.indexBase = 1;
state.indexType = 0xffffffffu;
state.userConfig[0x24a] = 7;
for (const auto flags : {2u, 0x22u}) {
const auto draw = AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {3, flags}), state);
check(!draw.indexed && draw.indexAddress == 0 && draw.indexSize == 0, "auto draw used the index buffer");
check(draw.indexCount == 3 && draw.instanceCount == 4 && draw.firstVertex == 7 && draw.firstInstance == 0 && draw.flags == (flags & 0x20u), "auto draw parameters mismatch");
}
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}), 0x20); }, "compute queue");
for (const auto flags : {0u, 1u, 3u, 0x20u, 0x42u}) {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, flags}), 0); }, "auto draw flags");
}
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 2), 0); }, "header flags");
state.userConfig[0x24a] = std::numeric_limits<std::uint32_t>::max();
check(AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {1, 2}), state).firstVertex == std::numeric_limits<std::uint32_t>::max(), "last vertex rejected");
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {2, 2}), state); }, "vertex range overflow");
check(AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {0, 2}), state).indexCount == 0, "empty auto draw rejected");
state.userConfig.erase(0x24a);
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {1, 2}), state); }, "GE_INDX_OFFSET");
}
void testIndexedDraw() {
AgcDriver::QueueState state;
alignas(4) std::array<std::uint32_t, 8> indices{};
state.indexBase = reinterpret_cast<std::uintptr_t>(indices.data());
state.instanceCount = 3;
const auto packet = makePacket(0x35, {4, 2, 4, 0x20});
for (std::uint32_t type = 0; type < 3; ++type) {
state.indexType = type;
const auto draw = AgcDriver::Pm4::ResolveDraw(packet, state);
const auto size = type == 0 ? 2u : type == 1 ? 4u : 1u;
check(draw.indexAddress == state.indexBase + 2 * size && draw.indexSize == size && draw.indexCount == 4 && draw.instanceCount == 3 && draw.flags == 0x20, "indexed draw state mismatch");
}
state.indexType = 0;
check(AgcDriver::Pm4::ResolveDraw(packet, state).firstVertex == 0, "indexed draw invented a base vertex");
state.userConfig[0x24a] = 0xd4d4;
const auto offsetDraw = AgcDriver::Pm4::ResolveDraw(packet, state);
check(offsetDraw.indexed && offsetDraw.firstVertex == 0xd4d4, "DRAW_INDEX_OFFSET_2 ignored GE_INDX_OFFSET");
const auto address = reinterpret_cast<std::uintptr_t>(indices.data());
const auto explicitDraw = AgcDriver::Pm4::ResolveDraw(makePacket(0x27, {4, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 4, 0}), state);
check(explicitDraw.indexed && explicitDraw.firstVertex == 0xd4d4 && explicitDraw.indexAddress == address, "DRAW_INDEX_2 ignored GE_INDX_OFFSET");
state.userConfig.erase(0x24a);
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "GE_INDX_OFFSET");
state.userConfig[0x24a] = 0;
expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0x20); }, "compute queue");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x35, {3, 0, 4, 0}), 0); }, "maximum index size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x35, {4, 0, 4, 1}), 0); }, "draw flags");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x35, {4, 0, 4}), 0); }, "packet size");
state.indexType = 3;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "index type");
state.indexType = 1;
state.indexBase += 1;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "misaligned index base");
state.indexBase = std::numeric_limits<std::uint64_t>::max() - 3;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "index address overflow");
state.indexType = 2;
state.indexBase = std::numeric_limits<std::uint64_t>::max() - 4;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "address range overflow");
state.indexBase = 0x1000;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "guest");
}
void testIndirectDraw() {
check(AgcDriver::Pm4::AccessesMemory(0xc0032400u) && AgcDriver::Pm4::AccessesMemory(0xc0083800u), "indirect draws must synchronize guest memory");
check(AgcDriver::Pm4::UnsupportedReason(0xc0032400u).empty() && AgcDriver::Pm4::UnsupportedReason(0xc0082c00u).empty(), "indirect draws are rejected");
AgcDriver::QueueState state;
state.userConfig[0x24a] = 5;
const auto packet = makePacket(0x24, {0, 0x280, 0x8e, 2});
AgcDriver::Pm4::Validate(packet, 0);
expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0x20); }, "compute queue");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {2, 0x280, 0x8e, 2}), 0); }, "misaligned indirect draw offset");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x10280, 0x8e, 2}), 0); }, "start-index location");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x200, 0x8e, 2}), 0); }, "register location");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x280, 0x8e, 0}), 0); }, "initiator");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x25, {0, 0x280, 0x8e, 2}), 0); }, "initiator");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x280, 0x8e}), 0); }, "packet size");
AgcDriver::Pm4::Validate(makePacket(0x2c, {0, 0x8c, 0x280, 0x8d | (1u << 31u), 3, 0, 0, 16, 2}), 0);
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2c, {0, 0x280, 0x8e, 0x280 | (1u << 27u), 3, 0, 0, 16, 2}), 0); }, "control bits");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2c, {0, 0x280, 0x8e, 0x280, 3, 0, 0, 12, 2}), 0); }, "stride");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x38, {0, 0x280, 0x8e, 0x280, 3, 0, 0, 16, 0}), 0); }, "stride");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2c, {0, 0x280, 0x8e, 0x280 | (1u << 30u), 3, 0, 0, 16, 2}), 0); }, "count address");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2c, {0, 0x280, 0x8e, 0x280, 3, 0x1000, 0, 16, 2}), 0); }, "count address");
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(packet, state); }, "base has not been set");
execute(state, makePacket(0x11, {1, 0x5d87fc40, 0x10}));
auto draw = AgcDriver::Pm4::ResolveDraw(packet, state);
check(draw.indirect.has_value() && draw.indirect->arguments == 0x105d87fc40ull && draw.indirect->opcode == 0x24 && draw.indirect->recordBytes == 16 && draw.indirect->stride == 16 && draw.indirect->count == 1 && !draw.indirect->countIndirect, "indirect draw arguments mismatch");
check(draw.indirect->baseVertexLocation == 0x280 && draw.indirect->startInstanceLocation == 0x8e && draw.indirect->drawIndexLocation == 0x280 && !draw.indirect->drawIndexEnabled && draw.indirect->indxOffset == 5 && draw.firstVertex == 5 && !draw.indexed && draw.indexCount == 0 && draw.instanceCount == 0, "indirect draw locations mismatch");
check(draw.indirect->RangeBytes() == 16 && draw.indirect->VertexDwordOffset() == 8 && draw.indirect->InstanceDwordOffset() == 12, "indirect draw record geometry mismatch");
check(AgcDriver::Pm4::ResolveDraw(makePacket(0x24, {0x60, 0x280, 0x8e, 2}), state).indirect->arguments == 0x105d87fca0ull, "indirect draw offset not applied");
AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x8d, 0, 2}), 0);
AgcDriver::Pm4::Validate(makePacket(0x11, {1, 0x10400014, 0x40}), 0);
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x11, {1, 0x10400012, 0x40}), 0); }, "alignment");
const auto unused = AgcDriver::Pm4::ResolveDraw(makePacket(0x24, {0, 0x8d, 0, 2}), state);
check(unused.indirect->baseVertexLocation == 0x8d && unused.indirect->startInstanceLocation == 0x280, "an indirect draw location 0 did not mean no register");
const auto unusedMulti = AgcDriver::Pm4::ResolveDraw(makePacket(0x2c, {0x20, 0x8c, 0, 0, 3, 0, 0, 32, 0x22}), state);
check(unusedMulti.indirect->startInstanceLocation == 0x280 && unusedMulti.indirect->drawIndexLocation == 0x280, "a multi indirect draw location 0 did not mean no register");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x24, {0, 0x8d, 0x200, 2}), 0); }, "invalid indirect draw register location");
const auto multi = AgcDriver::Pm4::ResolveDraw(makePacket(0x2c, {0x20, 0x8c, 0x280, 0x8d | (1u << 31u), 3, 0, 0, 32, 0x22}), state);
check(multi.indirect->count == 3 && multi.indirect->stride == 32 && multi.indirect->drawIndexEnabled && multi.indirect->drawIndexLocation == 0x8d && !multi.indirect->countIndirect && multi.flags == 0x20 && multi.indirect->RangeBytes() == 80, "indirect multi-draw mismatch");
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state); }, "index base");
alignas(4) std::array<std::uint16_t, 8> indices{};
state.indexBase = reinterpret_cast<std::uintptr_t>(indices.data());
state.indexType = 0;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state); }, "INDEX_BUFFER_SIZE");
state.indexBufferSize = 8;
const auto indexed = AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state);
check(indexed.indexed && indexed.indexAddress == state.indexBase && indexed.indexCount == 8 && indexed.indexSize == 2 && indexed.indirect->recordBytes == 20 && indexed.indirect->stride == 20 && indexed.firstVertex == 0 && indexed.indirect->indxOffset == 0, "indexed indirect draw mismatch");
check(indexed.indirect->VertexDwordOffset() == 12 && indexed.indirect->InstanceDwordOffset() == 16, "indexed record geometry mismatch");
alignas(16) std::array<std::uint32_t, 10> records{3, 2, 7, 9, 4, 1, 5, 6, 0, 0};
state.drawIndirectBase = reinterpret_cast<std::uintptr_t>(records.data());
const auto local = AgcDriver::Pm4::ResolveDraw(makePacket(0x2c, {0, 0x280, 0x280, 0x280, 2, 0, 0, 16, 2}), state);
const auto first = AgcDriver::Pm4::ReadDrawArguments(*local.indirect, 0);
const auto second = AgcDriver::Pm4::ReadDrawArguments(*local.indirect, 1);
check(first.count == 3 && first.instances == 2 && first.firstVertexOrIndex == 7 && first.vertexOffset == 0 && first.firstInstance == 9, "non-indexed record layout mismatch");
check(second.count == 4 && second.instances == 1 && second.firstVertexOrIndex == 5 && second.firstInstance == 6, "second record mismatch");
expectFailure([&] { AgcDriver::Pm4::ReadDrawArguments(*local.indirect, 2); }, "record index");
records = {3, 2, 7, 9, 11, 0, 0, 0, 0, 0};
const auto indexedRecord = AgcDriver::Pm4::ReadDrawArguments(*AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state).indirect, 0);
check(indexedRecord.count == 3 && indexedRecord.instances == 2 && indexedRecord.firstVertexOrIndex == 7 && indexedRecord.vertexOffset == 9 && indexedRecord.firstInstance == 11, "indexed record layout mismatch");
alignas(4) std::uint32_t countValue = 2;
const auto counted = AgcDriver::Pm4::ResolveDraw(makePacket(0x2c, {0, 0x280, 0x280, 0x280 | (1u << 30u), 5, low(&countValue), high(&countValue), 16, 2}), state);
check(counted.indirect->countIndirect && counted.indirect->count == 5 && counted.indirect->countAddress == reinterpret_cast<std::uintptr_t>(&countValue) && AgcDriver::Pm4::ReadDrawCount(*counted.indirect) == 2, "indirect draw count mismatch");
expectFailure([&] { AgcDriver::Pm4::ReadDrawCount(*local.indirect); }, "count address");
}
bool sameDraw(const AgcDriver::Pm4::DrawParameters& left, const AgcDriver::Pm4::DrawParameters& right) {
return left.indexAddress == right.indexAddress && left.indexCount == right.indexCount && left.indexSize == right.indexSize && left.instanceCount == right.instanceCount &&
left.flags == right.flags && left.indexed == right.indexed && left.firstVertex == right.firstVertex && left.indirect.has_value() == right.indirect.has_value();
}
void testIndexAttributesIndirect() {
check(AgcDriver::Pm4::Name(0xc0029100u) == "INDEX_ATTRIBUTES_INDIRECT" && AgcDriver::Pm4::UnsupportedReason(0xc0029100u).empty(), "INDEX_ATTRIBUTES_INDIRECT is not executed");
check(AgcDriver::Pm4::AccessesMemory(0xc0029100u), "INDEX_ATTRIBUTES_INDIRECT must synchronize guest memory");
alignas(4) static std::array<std::uint16_t, 32> indices{};
const auto indexAddress = reinterpret_cast<std::uintptr_t>(indices.data()) + 4;
alignas(16) static std::array<std::uint32_t, 12> records{};
records.fill(0xdeadbeefu);
records[4] = static_cast<std::uint32_t>(indexAddress);
records[5] = static_cast<std::uint32_t>(indexAddress >> 32u);
records[6] = 12;
records[7] = 1;
const auto packetFor = [](const void* base, std::uint32_t index) { return makePacket(0x91, {low(base), high(base), index}); };
const auto packet = packetFor(records.data(), 1);
AgcDriver::QueueState loaded;
loaded.indexBase = 0x1234;
loaded.indexBufferSize = 77;
loaded.indexType = 2;
execute(loaded, packet);
check(loaded.indexBase == indexAddress && loaded.indexBufferSize == 12 && loaded.indexType == 1, "INDEX_ATTRIBUTES_INDIRECT did not load INDEX_BASE, INDEX_BUFFER_SIZE and INDEX_TYPE from its record");
AgcDriver::QueueState direct;
execute(direct, makePacket(0x26, {static_cast<std::uint32_t>(indexAddress), static_cast<std::uint32_t>(indexAddress >> 32u)}));
execute(direct, makePacket(0x13, {12}));
execute(direct, makePacket(0x2a, {1}));
const auto offsetDraw = makePacket(0x35, {12, 1, 5, 0});
const auto fromRecord = AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded);
check(sameDraw(fromRecord, AgcDriver::Pm4::ResolveDraw(offsetDraw, direct)) && fromRecord.indexAddress == indexAddress + 4 && fromRecord.indexSize == 4 && fromRecord.indexCount == 5, "an indexed draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets");
execute(loaded, makePacket(0x11, {1, low(records.data()), high(records.data())}));
execute(direct, makePacket(0x11, {1, low(records.data()), high(records.data())}));
const auto indirectDraw = makePacket(0x25, {0, 0x8c, 0x280, 0});
const auto indirectFromRecord = AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded);
check(sameDraw(indirectFromRecord, AgcDriver::Pm4::ResolveDraw(indirectDraw, direct)) && indirectFromRecord.indexAddress == indexAddress && indirectFromRecord.indexCount == 12 && indirectFromRecord.indexSize == 4, "an indexed indirect draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets");
records[6] = 0x7fffffffu;
execute(loaded, packet);
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "guest");
records[6] = 12;
static std::size_t flushed = 0;
flushed = 0;
struct FlushHookReset {
~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); }
} reset;
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) {
check(address == reinterpret_cast<std::uintptr_t>(records.data() + 4) && bytes == 16, "INDEX_ATTRIBUTES_INDIRECT synchronized an unrelated range");
records[6] = 9;
++flushed;
});
execute(loaded, packet);
check(flushed == 1 && loaded.indexBufferSize == 9, "INDEX_ATTRIBUTES_INDIRECT read its record before the GPU work that writes it");
AgcDriver::GuestMemory::SetFlushHook(nullptr);
records[6] = 12;
const auto unchanged = [&](const std::vector<std::uint32_t>& rejected, const char* text) {
AgcDriver::QueueState state;
state.indexBase = 0x40;
state.indexBufferSize = 3;
state.indexType = 2;
expectFailure([&] { execute(state, rejected); }, text);
check(state.indexBase == 0x40 && state.indexBufferSize == 3 && state.indexType == 2, "a rejected INDEX_ATTRIBUTES_INDIRECT changed the index state");
};
unchanged(makePacket(0x91, {low(records.data()) | 1u, high(records.data()), 1}), "indirect mode");
unchanged(makePacket(0x91, {low(records.data()) | 4u, high(records.data()), 1}), "16-byte aligned");
unchanged(makePacket(0x91, {low(records.data()), high(records.data()), 0x10001u}), "reserved attribute index bits");
unchanged(makePacket(0x91, {low(records.data()), high(records.data())}), "packet size");
unchanged(packetFor(nullptr, 0), "null or misaligned");
unchanged(packetFor(reinterpret_cast<const void*>(0x1000), 0), "guest");
unchanged(makePacket(0x91, {0xfffffff0u, 0xffffffffu, 1}), "record address overflow");
expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0x20); }, "compute queue");
records[7] = 4;
unchanged(packet, "has index type 0x4");
char named[64];
std::snprintf(named, sizeof(named), "record at 0x%llx", static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(records.data() + 4)));
unchanged(packet, named);
records[7] = 3;
execute(loaded, packet);
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); }, "unsupported index type");
records[7] = 1;
}
void testMemory() {
AgcDriver::QueueState state;
std::array<std::uint32_t, 4> data{0, 0, 0, 0};
execute(state, makePacket(0x37, {0x100, low(data.data()), high(data.data()), 11, 12}));
check(data[0] == 11 && data[1] == 12, "WRITE_DATA increment failed");
execute(state, makePacket(0x37, {0x10100, low(data.data()), high(data.data()), 21, 22}));
check(data[0] == 22 && data[1] == 12, "WRITE_DATA fixed destination failed");
execute(state, makePacket(0x37, {0x40000100, low(data.data()), high(data.data()), 41, 42}));
check(data[0] == 41 && data[1] == 42, "WRITE_DATA from the PFP failed");
execute(state, makePacket(0x37, {0x04100200, low(data.data()), high(data.data()), 61, 62}));
check(data[0] == 61 && data[1] == 62, "WRITE_DATA with a cache policy failed");
execute(state, makePacket(0x37, {0x40100500, low(data.data()), high(data.data()), 81, 82}));
check(data[0] == 81 && data[1] == 82, "WRITE_DATA to memory from the graphics queue failed");
execute(state, makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 91, 92}));
check(data[0] == 91 && data[1] == 92, "WRITE_DATA with destination 3 did not write memory");
const auto fence = makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 0});
const auto fenceLabel = AgcDriver::Pm4::DecodeLabelWrite(fence);
check(fenceLabel.has_value() && fenceLabel->address == reinterpret_cast<std::uint64_t>(data.data()), "WRITE_DATA with destination 3 did not resolve as a label");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x100000, low(data.data()), high(data.data()), 1}), 0); }, "register destination");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x08000100, low(data.data()), high(data.data()), 71}), 0); }, "reserved");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x80000100, low(data.data()), high(data.data()), 51}), 0); }, "engine");
execute(state, makePacket(0x83, {0xbff8, 2, low(data.data()), high(data.data())}));
check(data[0] == 0 && data[1] == 0, "unwritten constant RAM is not zero");
execute(state, makePacket(0x81, {4, 31, 32}));
execute(state, makePacket(0x83, {4, 2, low(data.data()), high(data.data())}));
check(data[0] == 31 && data[1] == 32, "constant RAM round trip failed");
auto copied = state;
execute(copied, makePacket(0x83, {0, 3, low(data.data()), high(data.data())}));
check(data[0] == 0 && data[1] == 31 && data[2] == 32, "a copied queue state lost its constant RAM");
expectFailure([&] { execute(state, makePacket(0x81, {0xbffc, 1, 2})); }, "overflow");
expectFailure([&] { execute(state, makePacket(0x40, {0x10105, 0, 0, low(data.data()), high(data.data())})); }, "64-bit immediate");
}
void testCopies() {
AgcDriver::QueueState state;
alignas(8) std::array<std::uint32_t, 4> source{11, 12, 13, 14};
alignas(8) std::array<std::uint32_t, 4> destination{};
execute(state, makePacket(0x40, {0x10101, low(source.data()), high(source.data()), low(destination.data()), high(destination.data())}));
check(destination[0] == 11 && destination[1] == 12 && destination[2] == 0, "64-bit COPY_DATA failed");
execute(state, makePacket(0x40, {0x105, 0x12345678, 0, low(destination.data()), high(destination.data())}));
check(destination[0] == 0x12345678, "immediate COPY_DATA failed");
execute(state, makePacket(0x40, {0x505, 0x7654321, 0, low(destination.data()), high(destination.data())}));
check(destination[0] == 0x7654321, "immediate COPY_DATA to DST_SEL 5 memory failed");
alignas(8) std::array<std::uint64_t, 1> stamp{};
execute(state, makePacket(0x40, {0x110509, 0, 0, low(stamp.data()), high(stamp.data())}));
check(stamp[0] != 0, "GPU timestamp COPY_DATA to DST_SEL 5 memory failed");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x305, 1, 0, 0, 0}), 0); }, "GDS or performance-counter destination");
alignas(8) std::array<std::uint32_t, 2> clearValues{0x55, 0x3f800000};
execute(state, makePacket(0x40, {0x10001, low(clearValues.data()), high(clearValues.data()), 0xa00a, 0}));
check(state.context.at(0x0a) == 0x55 && state.context.at(0x0b) == 0x3f800000, "COPY_DATA to two context registers failed");
execute(state, makePacket(0x40, {0x1, low(&clearValues[1]), high(&clearValues[1]), 0xa323, 0}));
check(state.context.at(0x323) == 0x3f800000 && !state.context.contains(0x324), "COPY_DATA to one context register failed");
check(!AgcDriver::Pm4::ResolveStore(makePacket(0x40, {0x1, low(clearValues.data()), high(clearValues.data()), 0xa00c, 0}), state, 64).has_value(), "COPY_DATA to a register resolved as a memory store");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x5, 7, 0, 0xa00a, 0}), 0); }, "to a register from a register, GDS, immediate");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1, low(clearValues.data()), high(clearValues.data()), 0x2e40, 0}), 0); }, "outside the context registers");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x10001, low(clearValues.data()), high(clearValues.data()), 0xa3ff, 0}), 0); }, "outside the context registers");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1, low(clearValues.data()) + 2, high(clearValues.data()), 0xa00a, 0}), 0); }, "misaligned COPY_DATA source");
alignas(8) std::array<std::uint64_t, 2> clock{};
const auto clockCopy = [&](std::uint64_t* target) { return makePacket(0x40, {0x06016209, 0, 0, low(target), high(target)}); };
execute(state, clockCopy(&clock[0]));
execute(state, clockCopy(&clock[1]));
check(clock[0] != 0 && clock[1] >= clock[0], "GPU clock COPY_DATA failed");
alignas(8) std::array<std::uint32_t, 2> clock32{0, 0xdeadbeef};
execute(state, makePacket(0x40, {0x06006209, 0, 0, low(clock32.data()), high(clock32.data())}));
check(clock32[0] != 0 && clock32[1] == 0xdeadbeef, "32-bit GPU clock COPY_DATA did not write only the low half");
const auto clockStore =AgcDriver::Pm4::ResolveStore(clockCopy(&clock[0]), state, 64);
check(clockStore.has_value() && clockStore->Bytes().size() == 8, "GPU clock COPY_DATA did not resolve as an 8-byte store");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1020a, 0, 0, low(&clock[0]), high(&clock[0])}), 0); }, "reference-clock");
alignas(8) std::array<std::uint32_t, 2> fromRegister{0xdeadbeef, 0xdeadbeef};
const auto registerCopy = [&](std::uint32_t control, std::uint32_t offset) { return makePacket(0x40, {control, offset, 0, low(fromRegister.data()), high(fromRegister.data())}); };
execute(state, makePacket(0x76, {0x20c, 0x02754800, 0x2}));
execute(state, registerCopy(0x00100200, 0x2e0c));
check(fromRegister[0] == 0x02754800 && fromRegister[1] == 0xdeadbeef, "COPY_DATA from a shader register failed");
execute(state, registerCopy(0x00110200, 0x2e0c));
check(fromRegister[0] == 0x02754800 && fromRegister[1] == 0x2, "64-bit COPY_DATA from two shader registers failed");
execute(state, makePacket(0x69, {0x10, 0xabc}));
execute(state, registerCopy(0x00100200, 0xa010));
check(fromRegister[0] == 0xabc, "COPY_DATA from a context register failed");
check(!AgcDriver::Pm4::ResolveStore(registerCopy(0x00100200, 0x2e0c), state, 64).has_value(), "COPY_DATA from a register resolved before its turn");
AgcDriver::Pm4::NoteDispatchRegisters(makePacket(0x15, {4, 2, 1, 0x8041}), state);
execute(state, registerCopy(0x00100200, 0x2e00));
check(fromRegister[0] == 0x8041, "COPY_DATA did not read the last dispatch initiator");
execute(state, registerCopy(0x00100200, 0x2e01));
check(fromRegister[0] == 4, "COPY_DATA did not read the last direct dispatch's group count");
AgcDriver::Pm4::NoteDispatchRegisters(makePacket(0x16, {0, 0x8001}), state);
execute(state, registerCopy(0x00100200, 0x2e00));
check(fromRegister[0] == 0x8001, "COPY_DATA did not read the last indirect dispatch initiator");
expectFailure([&] { execute(state, registerCopy(0x00100200, 0x2e01)); }, "has no known value");
expectFailure([&] { execute(state, registerCopy(0x00100200, 0x2e3f)); }, "has no known value");
expectFailure([&] { AgcDriver::Pm4::Validate(registerCopy(0x00100200, 0x1000), 0); }, "outside the shader, context and user config");
expectFailure([&] { AgcDriver::Pm4::Validate(registerCopy(0x00110200, 0x2fff), 0); }, "outside the shader, context and user config");
execute(state, makePacket(0x50, {0x60000000, low(source.data()), high(source.data()), low(destination.data()), high(destination.data()), 16}));
check(source == destination, "DMA_DATA copy failed");
execute(state, makePacket(0x50, {0x40000000, 0x44332211, 0, low(destination.data()), high(destination.data()), 6}));
check(destination[0] == 0x44332211 && destination[1] == 0x00002211, "DMA_DATA byte fill failed");
constexpr std::uint32_t cachePolicies = (1u << 13u) | (2u << 25u);
std::size_t flexibleBefore = 0;
check(sceKernelAvailableFlexibleMemorySize(&flexibleBefore) == 0, "cannot query flexible memory");
const auto toGds = makePacket(0x50, {0x60100000 | cachePolicies, low(source.data()), high(source.data()), 0x100, 0, 16});
check(!AgcDriver::Pm4::ResolveStore(toGds, state, 64).has_value(), "DMA_DATA to GDS resolved as a memory store");
execute(state, toGds);
std::size_t flexibleAfter = 0;
check(sceKernelAvailableFlexibleMemorySize(&flexibleAfter) == 0 && flexibleAfter == flexibleBefore, "the GDS was charged to the flexible memory budget");
execute(state, makePacket(0x50, {0x20100000, 0x104, 0, 0xfff8, 0, 8}));
destination = {};
execute(state, makePacket(0x50, {0x20000000 | cachePolicies, 0xfff8, 0, low(destination.data()), high(destination.data()), 8}));
check(destination[0] == 12 && destination[1] == 13 && destination[2] == 0, "DMA_DATA GDS to GDS round trip failed");
const auto fromGds = makePacket(0x50, {0x20000000, 0x100, 0, low(destination.data()), high(destination.data()), 16});
const auto store = AgcDriver::Pm4::ResolveStore(fromGds, state, 64);
check(store.has_value() && store->Bytes().size() == 16 && std::memcmp(store->Bytes().data(), source.data(), 16) == 0, "DMA_DATA from GDS did not resolve its source bytes");
destination = {};
execute(state, fromGds);
check(source == destination, "DMA_DATA GDS round trip failed");
expectFailure([&] { execute(state, makePacket(0x50, {0x60100000, low(source.data()), high(source.data()), 0xfffc, 0, 8})); }, "exceeds the GDS");
expectFailure([&] { execute(state, makePacket(0x50, {0x20000000, 0, 1, low(destination.data()), high(destination.data()), 4})); }, "exceeds the GDS");
destination = {};
const auto prefetch = makePacket(0x50, {0x60200000, low(source.data()), high(source.data()), low(source.data()), high(source.data()), 0x80000010});
const auto prefetchStore = AgcDriver::Pm4::ResolveStore(prefetch, state, 64);
check(prefetchStore.has_value() && prefetchStore->Bytes().empty(), "a DMA_DATA prefetch resolved as a memory store");
check(!AgcDriver::Pm4::DecodeMemoryCopy(prefetch).has_value(), "a DMA_DATA prefetch decoded as a copy");
execute(state, prefetch);
check(source[0] == 11 && source[1] == 12 && destination[0] == 0, "a DMA_DATA prefetch wrote memory");
expectFailure([&] { execute(state, makePacket(0x50, {0x40200000, 1, 0, 0, 0, 4})); }, "prefetch of a register");
expectFailure([&] { execute(state, makePacket(0x50, {0x60200000, low(source.data()), high(source.data()), 0, 0, 4 | (1u << 27u)})); }, "register destination");
expectFailure([&] { execute(state, makePacket(0x50, {0x60000000 | (1u << 15u), low(source.data()), high(source.data()), low(destination.data()), high(destination.data()), 4})); }, "reserved fields");
expectFailure([&] { execute(state, makePacket(0x37, {0x100, 0x1000, 0, 1})); }, "guest");
#ifdef _WIN32
auto* memory = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
check(memory != nullptr, "VirtualAlloc failed");
DWORD previous = 0;
check(VirtualProtect(memory, 4096, PAGE_READONLY, &previous) != 0, "VirtualProtect failed");
try {
expectFailure([&] { execute(state, makePacket(0x37, {0x100, low(memory), high(memory), 1})); }, "write permission");
} catch (...) { VirtualFree(memory, 0, MEM_RELEASE); throw; }
check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
#endif
}
void testTraceRegisterWrites() {
AgcDriver::QueueState state;
const auto repeated = makePacket(0x37, {0x06010000u, 0xc343u, 0u, 0x1234u, 0x5678u});
check(!AgcDriver::Pm4::DecodeLabelWrite(repeated), "trace register payload was decoded as a guest memory label");
execute(state, repeated);
check(state.userConfig.at(0x343u) == 0x5678u, "one-address trace write did not retain the last register value");
execute(state, makePacket(0x37, {0x06000000u, 0xc340u, 0u, 1u, 2u, 3u}));
check(state.userConfig.at(0x340u) == 1u && state.userConfig.at(0x341u) == 2u && state.userConfig.at(0x342u) == 3u, "sequential trace write did not advance the register address");
execute(state, makePacket(0x37, {0x06010000u, 0xc347u, 0u, 4u, 5u}));
check(state.userConfig.at(0x347u) == 5u, "one-address trace write crossed the register range");
for (const auto& invalid : {makePacket(0x37, {0x06000000u, 0xc347u, 0u, 1u, 2u}), makePacket(0x37, {0x06010000u, 0xc348u, 0u, 1u}), makePacket(0x37, {0x06010000u, 0xc343u, 1u, 1u})}) {
expectFailure([&] { execute(state, invalid); }, "register destination outside");
}
}
void testMemoryCopyDecode() {
using AgcDriver::Pm4::DecodeMemoryCopy;
constexpr std::uint64_t source = 0x1120000000ull, destination = 0x403d7b400ull;
const auto packet = [&](std::uint32_t control, std::uint64_t from, std::uint64_t to, std::uint32_t command) {
return makePacket(0x50, {control, static_cast<std::uint32_t>(from), static_cast<std::uint32_t>(from >> 32u), static_cast<std::uint32_t>(to), static_cast<std::uint32_t>(to >> 32u), command});
};
const auto copy = DecodeMemoryCopy(packet(0x60000000, source, destination, 0xbdd800));
check(copy.has_value() && copy->source == source && copy->destination == destination && copy->bytes == 0xbdd800, "a memory-to-memory DMA_DATA did not decode as a copy");
check(DecodeMemoryCopy(packet(0x00000000, source, destination, 64)).has_value(), "a DMA_DATA with memory selectors 0 did not decode as a copy");
check(!DecodeMemoryCopy(packet(0x40000000, 0x44332211, destination, 64)).has_value(), "an immediate fill decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60100000, source, 0x100, 64)).has_value(), "a DMA_DATA to the GDS decoded as a copy");
check(!DecodeMemoryCopy(packet(0x20000000, 0x100, destination, 64)).has_value(), "a DMA_DATA from the GDS decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source, destination, 64 | (1u << 26u))).has_value(), "a register source decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source, destination, 64 | (1u << 27u))).has_value(), "a register destination decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source, destination, 64 | (1u << 28u))).has_value(), "a non-incrementing source decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source, destination, 64 | (1u << 29u))).has_value(), "a non-incrementing destination decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source, destination, 0)).has_value(), "an empty DMA_DATA decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source, source + 32, 64)).has_value(), "overlapping ranges decoded as a copy");
check(!DecodeMemoryCopy(packet(0x60000000, source + 32, source, 64)).has_value(), "overlapping ranges below the source decoded as a copy");
check(DecodeMemoryCopy(packet(0x60000000, source, source + 64, 64)).has_value(), "adjacent ranges did not decode as a copy");
check(!DecodeMemoryCopy(makePacket(0x40, {0x10101, 0, 0, 0, 0})).has_value(), "a COPY_DATA decoded as a DMA_DATA copy");
}
void testMemorySynchronization() {
struct MemoryState {
std::uint32_t source = 0;
std::uint32_t destination = 0;
bool read = false;
bool written = false;
};
static MemoryState memory;
memory = {};
AgcDriver::QueueState state;
struct FlushHookReset {
~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); }
} reset;
const auto resolve = [](std::uint64_t address, std::size_t bytes) {
check(bytes == sizeof(std::uint32_t), "memory transfer resolved an unrelated range");
if (address == reinterpret_cast<std::uintptr_t>(&memory.destination)) {
memory.written = true;
} else {
check(address == reinterpret_cast<std::uintptr_t>(&memory.source), "memory transfer resolved an unrelated source");
memory.source = 42;
memory.read = true;
}
};
AgcDriver::GuestMemory::SetFlushHook(resolve);
execute(state, makePacket(0x37, {0x100, low(&memory.destination), high(&memory.destination), 17}));
check(memory.written && !memory.read && memory.destination == 17, "WRITE_DATA did not synchronize its destination");
for (const auto opcode : {0x40u, 0x50u}) {
memory = {};
const auto packet = opcode == 0x40
? makePacket(opcode, {0x101, low(&memory.source), high(&memory.source), low(&memory.destination), high(&memory.destination)})
: makePacket(opcode, {0x60000000, low(&memory.source), high(&memory.source), low(&memory.destination), high(&memory.destination), 4});
execute(state, packet);
check(memory.read && memory.written && memory.destination == 42, "memory copy used stale data before range synchronization");
}
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t, std::size_t) {
throw std::runtime_error("range synchronization failed");
});
expectFailure([&] { execute(state, makePacket(0x37, {0x100, low(&memory.destination), high(&memory.destination), 99})); }, "range synchronization failed");
check(memory.destination == 42, "failed synchronization changed the destination");
}
void testConditionalValidation() {
alignas(4) static std::uint32_t condition = 0;
const auto valid = makePacket(0x22, {low(&condition), high(&condition), 0, 0x3fff});
check(AgcDriver::Pm4::UnsupportedReason(valid[0]).empty() && AgcDriver::Pm4::AccessesMemory(valid[0]), "COND_EXEC is rejected or does not synchronize guest memory");
AgcDriver::Pm4::Validate(valid, 0);
AgcDriver::Pm4::Validate(valid, 0x20);
check(AgcDriver::Pm4::ConditionalWords(valid) == 0x3fff, "COND_EXEC count decoded wrong");
check(AgcDriver::Pm4::ConditionalWords(makePacket(0x22, {low(&condition), high(&condition), 0, 0})) == 0, "empty COND_EXEC range decoded wrong");
AgcDriver::Pm4::Validate(makePacket(0x22, {low(&condition), high(&condition), 3u << 25u, 5}), 0x20);
const auto invalidWord = [&](std::size_t word, std::uint32_t value, std::uint32_t queue, const char* text) {
auto packet = valid;
packet[word] = value;
expectFailure([&] { AgcDriver::Pm4::Validate(packet, queue); }, text);
};
invalidWord(1, low(&condition) | 1u, 0, "reserved address bits");
invalidWord(1, low(&condition) | 2u, 0x20, "reserved address bits");
invalidWord(2, 0x10000u, 0, "above 48");
invalidWord(3, 3u << 25u, 0, "reserved control fields");
invalidWord(3, 1u, 0x20, "reserved control fields");
invalidWord(3, 1u << 27u, 0x20, "reserved control fields");
invalidWord(4, 0x4000u, 0, "reserved count bits");
invalidWord(4, 0x80000005u, 0x20, "reserved count bits");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x22, {0, 0, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x22, {0, 0, 0, 0, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x22, {0, 0, 0, 0}, 1), 0); }, "header flags");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x22, {0, 0, 0, 0}, 2), 0x20); }, "header flags");
expectFailure([] { AgcDriver::Pm4::ConditionalWords(makePacket(0x37, {0x100, 0, 0, 0})); }, "expected COND_EXEC");
AgcDriver::QueueState state;
expectFailure([&] { AgcDriver::Pm4::Execute(valid, state); }, "driver execution");
}
void testConditionReadSynchronization() {
alignas(4) static std::uint32_t condition = 0;
static std::size_t flushed = 0;
condition = 0;
flushed = 0;
struct FlushHookReset {
~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); }
} reset;
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) {
check(address == reinterpret_cast<std::uintptr_t>(&condition) && bytes == sizeof(condition), "COND_EXEC synchronized an unrelated range");
condition = 0x80;
++flushed;
});
const auto packet = makePacket(0x22, {low(&condition), high(&condition), 0, 5});
check(AgcDriver::Pm4::ReadCondition(packet) == 0x80 && flushed == 1, "COND_EXEC read its condition before the GPU work that writes it");
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t, std::size_t) {
throw std::runtime_error("condition synchronization failed");
});
expectFailure([&] { AgcDriver::Pm4::ReadCondition(packet); }, "condition synchronization failed");
AgcDriver::GuestMemory::SetFlushHook(nullptr);
expectFailure([] { AgcDriver::Pm4::ReadCondition(makePacket(0x22, {0x1000, 0, 0, 5})); }, "guest");
}
void testWriteChangedKeepsUntouchedBytes() {
alignas(256) static std::uint8_t guest[256];
std::memset(guest, 0, sizeof(guest));
std::vector<std::byte> original(sizeof(guest)), current(sizeof(guest));
current[3] = std::byte{7};
guest[100] = 0x55;
AgcDriver::GuestMemory::WriteChanged(reinterpret_cast<std::uintptr_t>(guest), current, original);
check(guest[3] == 7 && guest[100] == 0x55, "write-back rolled back a byte the GPU did not change");
}
void testWriteChangedStoresExactlyTheChangedRuns() {
alignas(256) static std::uint8_t guest[1024 + 13];
std::vector<std::byte> original(sizeof(guest)), current(sizeof(guest));
std::uint32_t seed = 12345;
const auto next = [&] { return seed = seed * 1103515245u + 12345u, seed >> 16u; };
for (std::size_t i = 0; i < sizeof(guest);) {
const auto length = 1 + next() % 40;
const bool changed = next() % 2 == 0;
for (std::size_t j = i; j < std::min(sizeof(guest), i + length); ++j) {
original[j] = static_cast<std::byte>(next());
current[j] = changed && next() % 9 != 0 ? static_cast<std::byte>(static_cast<std::uint8_t>(original[j]) ^ (1u + next() % 255u)) : original[j];
}
i += length;
}
for (std::size_t i = 0; i < sizeof(guest); ++i) guest[i] = static_cast<std::uint8_t>(0xa0u ^ i);
AgcDriver::GuestMemory::WriteChanged(reinterpret_cast<std::uintptr_t>(guest), current, original);
for (std::size_t i = 0; i < sizeof(guest); ++i) {
const auto expected = current[i] != original[i] ? static_cast<std::uint8_t>(current[i]) : static_cast<std::uint8_t>(0xa0u ^ i);
check(guest[i] == expected, "write-back stored a byte the GPU did not change or missed one it did");
}
}
void testEventWrite() {
for (const auto eventType : {0x07u, 0x0fu, 0x10u}) {
AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0);
for (std::uint32_t index = 0; index < 8; ++index) {
if (index == 4) continue;
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "partial-flush event index");
}
if (eventType == 0x07) {
AgcDriver::Pm4::Validate(makePacket(0x46, {0x407}), 0x20);
} else {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0x20); }, "compute queue");
}
}
for (const auto eventType : {0x16u, 0x31u, 0x2au, 0x2cu, 0x2eu}) {
for (const auto index : {0u, 7u}) {
AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0);
}
for (std::uint32_t index = 1; index < 7; ++index) {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "cache-flush event index");
}
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue");
}
AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000, 0x2}), 0);
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000, 0x2}), 0x20); }, "compute queue");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x039, 0x1000, 0x2}), 0); }, "counter dump event index");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1004, 0x2}), 0); }, "misaligned occlusion counter");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0, 0}), 0); }, "null or misaligned occlusion counter");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x138, 0x1000, 0x2}), 0); }, "event type 56");
for (const auto bit : {0x40u, 0x80u, 0x800u, 0x80000000u}) {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410u | bit}), 0); }, "reserved bits");
}
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 2), 0); }, "header flags");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410, 0, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x13a, 0, 0}), 0); }, "event type 58");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x0d}), 0); }, "event type 13");
for (const auto eventType : {0x19u, 0x1au, 0x26u}) {
AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0);
AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x100u | eventType}), 0); }, "pipeline statistics or SQ event index");
}
AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0);
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0x20); }, "compute queue");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x424}), 0); }, "VGT_FLUSH event index");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x1e, 0x1000, 0}), 0); }, "event type 30");
}
void testAcquireMem() {
const auto captured = makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200});
AgcDriver::Pm4::Validate(captured, 0);
check(AgcDriver::Pm4::UsesGpuCacheBarrier(captured), "L1 acquire must preserve GPU render targets");
for (const auto flags : {0u, 0x200u, 0x3ffu, 0x10200u, 0x20200u}) {
auto packet = captured;
packet[7] = flags;
check(AgcDriver::Pm4::UsesGpuCacheBarrier(packet), "GPU cache acquire requires an unnecessary host writeback");
}
for (const auto flags : {0x400u, 0x800u, 0x1000u, 0x4000u, 0x8000u}) {
auto packet = captured;
packet[7] = flags;
check(!AgcDriver::Pm4::UsesGpuCacheBarrier(packet), "L2 acquire lost host synchronization");
}
check(!AgcDriver::Pm4::UsesGpuCacheBarrier(makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10})), "legacy acquire lost host synchronization");
expectFailure([] { AgcDriver::Pm4::UsesGpuCacheBarrier({}); }, "requires ACQUIRE_MEM");
expectFailure([] { AgcDriver::Pm4::UsesGpuCacheBarrier(makePacket(0x58, {0, 0, 0, 0, 0, 0, 0x2000})); }, "cache discard");
AgcDriver::Pm4::Validate(makePacket(0x58, {0x82007fc0, 1, 0, 0xffffffff, 0, 0xffff, 0x200}), 0);
AgcDriver::Pm4::Validate(makePacket(0x58, {0x80000000, 0, 0, 0, 0, 10, 0x200}), 0x20);
AgcDriver::Pm4::Validate(makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}), 0);
AgcDriver::Pm4::Validate(makePacket(0x58, {0x80800000, 16, 0, 0x1000, 0, 0}), 0x20);
expectFailure([&] { AgcDriver::Pm4::Validate(captured, 0x20); }, "compute queue");
const auto invalidWord = [&](std::size_t index, std::uint32_t value, const char* reason) {
auto packet = captured;
packet[index] = value;
expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, reason);
};
invalidWord(0, captured[0] | 2u, "header flags");
invalidWord(1, 4, "control flags");
invalidWord(1, 0x00800000, "control flags");
invalidWord(3, 0x100, "range high bits");
invalidWord(5, 0x100, "range high bits");
invalidWord(6, 0x10000, "poll interval");
invalidWord(7, 0x40000, "GCR flags");
invalidWord(7, 0x2000, "cache discard");
AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0xffffffff, 0xff, 0, 0, 10, 0xc3b1}), 0);
AgcDriver::Pm4::Validate(makePacket(0x58, {0, 2, 0, 0xffffffff, 0, 0, 0}), 0);
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 2, 0, 0xffffffff, 0xff, 0, 0}), 0); }, "range exceeds");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0, 0, 0, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0, 0, 0, 0, 0, 0, 0}), 0); }, "packet size");
}
void testPredication() {
alignas(16) std::uint64_t flag[4] = {0, 0, 0, 0};
auto* flag32 = reinterpret_cast<std::uint32_t*>(&flag[2]);
const auto setPredication = [&](std::uint32_t operation, bool executeWhenSet, const void* address) {
return makePacket(0x20, {(operation << 16u) | (executeWhenSet ? 0x100u : 0u) | 0x1000u, low(address), high(address)});
};
AgcDriver::QueueState state;
check(AgcDriver::Pm4::PredicationPasses(state), "inactive predication must pass");
execute(state, setPredication(3, true, &flag[0]));
check(state.predication.operation == 3 && state.predication.executeWhenSet && state.predication.address == reinterpret_cast<std::uintptr_t>(&flag[0]), "BOOL64 predication state mismatch");
check(!AgcDriver::Pm4::PredicationPasses(state), "zero BOOL64 value must skip draw-visible packets");
flag[0] = 1ull << 40u;
check(AgcDriver::Pm4::PredicationPasses(state), "upper BOOL64 bits must count");
execute(state, setPredication(3, false, &flag[0]));
check(!AgcDriver::Pm4::PredicationPasses(state), "non-zero BOOL64 value must skip draw-not-visible packets");
flag[0] = 0;
check(AgcDriver::Pm4::PredicationPasses(state), "zero BOOL64 value must run draw-not-visible packets");
execute(state, setPredication(4, true, &flag[2]));
flag[2] = 0xffffffff00000000ull;
check(*flag32 == 0 && !AgcDriver::Pm4::PredicationPasses(state), "BOOL32 must read only 32 bits");
*flag32 = 7;
check(AgcDriver::Pm4::PredicationPasses(state), "non-zero BOOL32 value must run draw-visible packets");
execute(state, setPredication(0, false, nullptr));
check(state.predication.operation == 0 && AgcDriver::Pm4::PredicationPasses(state), "clear must end predication");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(1, true, &flag[0]), 0); }, "query predication");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(2, true, &flag[0]), 0); }, "query predication");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(5, true, &flag[0]), 0); }, "invalid predication operation");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, &flag[0]), 0x20); }, "compute queue");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, nullptr), 0); }, "unaligned predication address");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, reinterpret_cast<const std::uint8_t*>(&flag[0]) + 8), 0); }, "unaligned predication address");
auto continued = setPredication(3, true, &flag[0]);
continued[1] |= 1u << 31u;
expectFailure([&] { AgcDriver::Pm4::Validate(continued, 0); }, "SET_PREDICATION bits");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x20, {3u << 16u, low(&flag[0]), high(&flag[0])}, 1), 0); }, "header flags");
AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 1), 0);
AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0);
const auto call = makePacket(0x3f, {0x1000, 0, 0x0f200010}, 1);
AgcDriver::Pm4::Validate(call, 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x3f, {0x1000, 0, 0x0f200010}), 0); }, "nested command buffers");
expectFailure([&] { AgcDriver::Pm4::Execute(call, state); }, "nested command buffers");
}
void testDriverSubmission() {
std::array<std::uint32_t, 2> source{0x10, 73};
std::array<std::uint32_t, 1> destination{};
std::vector<std::uint32_t> commands;
for (const auto& packet : {
makePacket(0x9f, {low(source.data()), high(source.data()), 0x80000000, 1}),
makePacket(0x81, {0, 83}),
makePacket(0x42, {0}),
makePacket(0x46, {0x410}),
makePacket(0x46, {0x407}),
makePacket(0x46, {0x40f}),
makePacket(0x46, {0x16}),
makePacket(0x46, {0x731}),
makePacket(0x46, {0x2a}),
makePacket(0x46, {0x72c}),
makePacket(0x46, {0x2e}),
makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200}),
makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}),
makePacket(0x83, {0, 1, low(destination.data()), high(destination.data())})
}) commands.insert(commands.end(), packet.begin(), packet.end());
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "PM4 submission failed");
AgcDriverWaitIdle_nid_postfix();
check(destination[0] == 83, "worker did not execute PM4 memory operations");
auto rejectedCommands = commands;
const auto unsupportedEvent = makePacket(0x46, {0x0d});
rejectedCommands.insert(rejectedCommands.end(), unsupportedEvent.begin(), unsupportedEvent.end());
Packet rejectedPacket{rejectedCommands.data(), static_cast<std::uint32_t>(rejectedCommands.size()), 0, {}};
destination[0] = 0;
expectFailure([&] { sceAgcDriverSubmitDcb(&rejectedPacket); }, "EVENT_WRITE at DWORD");
AgcDriverWaitIdle_nid_postfix();
check(destination[0] == 0, "rejected event submission executed a prefix");
destination[0] = 0;
const auto emptyDraw = makePacket(0x2d, {0, 2});
commands.insert(commands.end(), emptyDraw.begin(), emptyDraw.end());
packet = Packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "auto draw submission failed");
AgcDriverWaitIdle_nid_postfix();
check(destination[0] == 83, "empty auto draw prevented command execution");
destination[0] = 0;
const auto draw = makePacket(0x2d, {3, 3});
commands.insert(commands.end(), draw.begin(), draw.end());
packet = Packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }, "DRAW_INDEX_AUTO at DWORD");
AgcDriverWaitIdle_nid_postfix();
check(destination[0] == 0, "rejected submission executed a prefix");
}
std::vector<std::uint32_t> joinPackets(std::initializer_list<std::vector<std::uint32_t>> packets) {
std::vector<std::uint32_t> words;
for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end());
return words;
}
std::vector<std::uint32_t> writeWord(std::uint32_t& target, std::uint32_t value) {
return makePacket(0x37, {0x00100200, low(&target), high(&target), value});
}
std::vector<std::uint32_t> conditional(const std::uint32_t& condition, std::uint32_t words, std::uint32_t control = 0) {
return makePacket(0x22, {low(&condition), high(&condition), control, words});
}
std::vector<std::uint32_t> indirectBuffer(const std::vector<std::uint32_t>& target, bool chain = false) {
return makePacket(0x3f, {low(target.data()), high(target.data()), static_cast<std::uint32_t>(target.size()) | (chain ? 1u << 20u : 0u)});
}
void submitWords(std::vector<std::uint32_t>& words, std::uint32_t queue = 0) {
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
check((queue == 0 ? sceAgcDriverSubmitDcb(&packet) : sceAgcDriverSubmitAcb(queue, &packet)) == 0, "conditional submission failed");
}
void testRegisterListsReadAtSubmission() {
alignas(8) static std::uint32_t gate = 0;
static std::uint32_t done = 0;
static std::array<std::uint32_t, 2> registers{0x10, 74};
auto words = joinPackets({
makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}),
makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 1}),
writeWord(done, 1)});
submitWords(words);
registers[0] = 0x3a888889;
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "register list rewritten after submission was read by the worker");
}
void testIndexAttributesReadAtExecution() {
alignas(8) static std::uint32_t gate = 0;
static std::uint32_t done = 0;
alignas(16) static std::array<std::uint32_t, 4> record{0, 0, 0, 4};
gate = 0;
done = 0;
auto words = joinPackets({
makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}),
makePacket(0x91, {low(record.data()), high(record.data()), 0}),
writeWord(done, 1)});
submitWords(words);
record = {0x1000, 0, 6, 0};
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "INDEX_ATTRIBUTES_INDIRECT did not read its record when the packet ran");
}
void testSuspendPointWritesQueuedLabels() {
alignas(8) static std::uint32_t gate = 0, marker = 0;
gate = 0;
marker = 0;
auto words = joinPackets({makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x190}),
makePacket(0x49, {0x0030c514, 0x20000000, low(&marker), high(&marker), 1, 0, 0})});
submitWords(words);
AgcDriverSuspendPoint_nid_postfix();
std::atomic_ref<std::uint32_t>(gate).store(1);
for (int waited = 0; waited < 2000 && std::atomic_ref<std::uint32_t>(marker).load() == 0; ++waited) std::this_thread::sleep_for(std::chrono::milliseconds(1));
check(std::atomic_ref<std::uint32_t>(marker).load() == 1, "a label queued before a suspend point was never written");
AgcDriverWaitIdle_nid_postfix();
}
void testConditionalSubmission() {
alignas(8) static std::uint32_t zero = 0, one = 1, condition = 0;
static std::array<std::uint32_t, 16> results{};
results.fill(0);
const auto run = [](std::vector<std::uint32_t> words, std::uint32_t queue = 0) {
submitWords(words, queue);
AgcDriverWaitIdle_nid_postfix();
};
run(joinPackets({conditional(one, 5), writeWord(results[0], 11), writeWord(results[1], 12)}));
check(results[0] == 11 && results[1] == 12, "COND_EXEC skipped a range whose condition is set");
run(joinPackets({conditional(zero, 6), {0x80000000u}, writeWord(results[0], 21), writeWord(results[1], 22)}));
check(results[0] == 11 && results[1] == 22, "COND_EXEC did not skip exactly its range when the condition is zero");
run(joinPackets({conditional(zero, 0), writeWord(results[2], 23)}));
check(results[2] == 23, "an empty COND_EXEC range skipped the next packet");
alignas(4) static std::uint32_t never = 0;
never = 0;
auto skippedWait = joinPackets({conditional(zero, 9), makePacket(0x3c, {0x13, low(&never), high(&never), 1, 0xffffffffu, 0x190}), makePacket(0x10, {0}, 0x30), writeWord(results[14], 24)});
submitWords(skippedWait);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
while (std::atomic_ref<std::uint32_t>(results[14]).load() != 24 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1));
const bool skipped = std::atomic_ref<std::uint32_t>(results[14]).load() == 24;
std::atomic_ref<std::uint32_t>(never).store(1);
AgcDriverWaitIdle_nid_postfix();
check(skipped, "a WAIT_REG_MEM inside a skipped COND_EXEC range was executed");
condition = 1;
run(joinPackets({writeWord(condition, 0), conditional(condition, 5), writeWord(results[0], 31), writeWord(results[1], 32)}));
check(condition == 0 && results[0] == 11 && results[1] == 32, "COND_EXEC read its condition before an earlier packet of its queue stored it");
run(joinPackets({writeWord(condition, 9), conditional(condition, 5), writeWord(results[0], 41)}));
check(results[0] == 41, "COND_EXEC skipped a range whose condition an earlier packet of its queue set");
condition = 0;
run(joinPackets({conditional(condition, 5, 3u << 25u), writeWord(results[3], 42), writeWord(condition, 1), conditional(condition, 5, 1u << 25u), writeWord(results[4], 43)}), 0x20);
check(results[3] == 0 && results[4] == 43, "compute queue COND_EXEC with a cache policy evaluated wrong");
run(joinPackets({conditional(one, 10), conditional(zero, 10), writeWord(results[5], 51), writeWord(results[6], 52), writeWord(results[7], 53)}));
check(results[5] == 0 && results[6] == 0 && results[7] == 53, "a nested COND_EXEC range reaching past the outer range was not skipped exactly");
run(joinPackets({conditional(zero, 10), conditional(one, 10), writeWord(results[5], 54), writeWord(results[6], 55), writeWord(results[7], 56)}));
check(results[5] == 0 && results[6] == 55 && results[7] == 56, "a skipped COND_EXEC range did not skip the COND_EXEC inside it");
static std::vector<std::uint32_t> target;
target = joinPackets({writeWord(results[8], 61), conditional(zero, 5), writeWord(results[9], 62), writeWord(results[10], 63)});
run(joinPackets({conditional(zero, 4), indirectBuffer(target), writeWord(results[11], 64)}));
check(results[8] == 0 && results[10] == 0 && results[11] == 64, "COND_EXEC over an INDIRECT_BUFFER did not skip the whole buffer");
run(joinPackets({conditional(one, 4), indirectBuffer(target), writeWord(results[11], 65)}));
check(results[8] == 61 && results[9] == 0 && results[10] == 63 && results[11] == 65, "COND_EXEC over an INDIRECT_BUFFER or inside one evaluated wrong");
const auto rejected = [](std::vector<std::uint32_t> words, const char* text) {
results[12] = 0;
expectFailure([&] { submitWords(words); }, text);
AgcDriverWaitIdle_nid_postfix();
check(results[12] == 0, "a rejected conditional submission executed a prefix");
};
const auto sentinel = writeWord(results[12], 1);
rejected(joinPackets({sentinel, conditional(one, 3), writeWord(results[13], 1)}), "ends inside a packet");
rejected(joinPackets({sentinel, conditional(one, 6), writeWord(results[13], 1)}), "exceeds its command buffer");
rejected(joinPackets({sentinel, conditional(one, 6), {0xc004105cu, 7, 0, 1, 0, 0}}), "a flip inside a conditional execution range");
rejected(joinPackets({sentinel, conditional(one, 2), makePacket(0x59, {0x80000000u})}), "REWIND inside a conditional execution range");
rejected(joinPackets({sentinel, conditional(one, 4), indirectBuffer(target, true)}), "chained INDIRECT_BUFFER");
rejected(joinPackets({sentinel, conditional(one, 2), {0x40000000u, 0}}), "whole packets");
rejected(joinPackets({sentinel, makePacket(0x22, {low(&one), high(&one), 0, 0x4005}), writeWord(results[13], 1)}), "reserved count bits");
static std::vector<std::uint32_t> unaligned, overlong;
unaligned = joinPackets({conditional(one, 3), writeWord(results[13], 1)});
overlong = joinPackets({conditional(one, 10), writeWord(results[13], 1)});
rejected(joinPackets({sentinel, indirectBuffer(unaligned)}), "ends inside a packet");
rejected(joinPackets({sentinel, indirectBuffer(overlong), writeWord(results[13], 1)}), "exceeds its command buffer");
check(results[13] == 0, "a rejected conditional submission executed a guarded packet");
}
void testNopPadSubmission() {
alignas(8) static std::uint32_t zero = 0;
static std::array<std::uint32_t, 3> results{};
results.fill(0);
auto words = joinPackets({{0xffff1000u, 0xc0027904u, 0x342u, 0xce200000u, 0u, 0xc0017904u, 0x342u, 0xcea00000u}, writeWord(results[0], 81), {0xffff1000u}});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(results[0] == 81, "a one-dword NOP stopped the packets after it");
words = joinPackets({conditional(zero, 6), {0xffff1000u}, writeWord(results[1], 82), writeWord(results[2], 83)});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(results[1] == 0 && results[2] == 83, "COND_EXEC did not count a one-dword NOP as one dword");
}
std::vector<std::uint32_t> branch(std::uint32_t mode, std::uint32_t function, const std::vector<std::uint32_t>* first, const std::vector<std::uint32_t>* second) {
const auto address = [](const std::vector<std::uint32_t>* target) { return target ? reinterpret_cast<std::uintptr_t>(target->data()) : std::uintptr_t{0}; };
const auto size = [](const std::vector<std::uint32_t>* target) { return target ? static_cast<std::uint32_t>(target->size()) : 0u; };
return makePacket(0x3f, {mode | (function << 8u), 0, 0, 0, 0, 0, 0, static_cast<std::uint32_t>(address(first)), static_cast<std::uint32_t>(address(first) >> 32u), size(first), static_cast<std::uint32_t>(address(second)), static_cast<std::uint32_t>(address(second) >> 32u), size(second)});
}
void testBranchSubmission() {
static std::array<std::uint32_t, 4> results{};
static std::vector<std::uint32_t> first, second;
results.fill(0);
first = joinPackets({writeWord(results[0], 71)});
second = joinPackets({writeWord(results[1], 72)});
auto words = joinPackets({branch(1, 0, &first, nullptr), writeWord(results[2], 73)});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(results[0] == 71 && results[1] == 0 && results[2] == 73, "an always-taken if-then COND_INDIRECT_BUFFER did not run its buffer");
results.fill(0);
words = joinPackets({branch(2, 0, &first, &second), writeWord(results[2], 74)});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(results[0] == 71 && results[1] == 0 && results[2] == 74, "an always-taken if-then-else COND_INDIRECT_BUFFER ran the wrong buffer");
results.fill(0);
words = joinPackets({branch(1, 0, nullptr, nullptr), writeWord(results[2], 75)});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(results[2] == 75, "an empty COND_INDIRECT_BUFFER skipped the next packet");
for (const auto& [mode, function, text] : {std::tuple{1u, 3u, "with a comparison"}, std::tuple{0u, 0u, "invalid COND_INDIRECT_BUFFER mode"}}) {
results.fill(0);
words = joinPackets({writeWord(results[3], 1), branch(mode, function, &first, nullptr)});
expectFailure([&] { submitWords(words); }, text);
AgcDriverWaitIdle_nid_postfix();
check(results[0] == 0 && results[3] == 0, "a rejected COND_INDIRECT_BUFFER submission executed a packet");
}
}
void testPredicatedSubmission() {
alignas(16) std::uint64_t flag[2] = {0, 0};
alignas(16) std::array<std::uint32_t, 4> written{};
alignas(16) std::array<std::uint32_t, 1> nestedWritten{};
const auto nested = makePacket(0x37, {0x100, low(nestedWritten.data()), high(nestedWritten.data()), 3});
const auto call = makePacket(0x3f, {low(nested.data()), high(nested.data()), 0x0f200000u | static_cast<std::uint32_t>(nested.size())}, 1);
const auto submit = [](std::vector<std::uint32_t>& commands) {
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "predicated submission failed");
AgcDriverWaitIdle_nid_postfix();
};
std::vector<std::uint32_t> commands;
for (const auto& packet : {
makePacket(0x20, {0x31100, low(&flag[0]), high(&flag[0])}),
makePacket(0x37, {0x100, low(&written[0]), high(&written[0]), 7}, 1),
call,
makePacket(0x20, {0, 0, 0}),
makePacket(0x37, {0x100, low(&written[1]), high(&written[1]), 9}, 1)
}) commands.insert(commands.end(), packet.begin(), packet.end());
submit(commands);
check(written[0] == 0, "packet with a false predicate was executed");
check(nestedWritten[0] == 0, "command buffer with a false predicate was executed");
check(written[1] == 9, "cleared predication skipped a packet");
flag[0] = 1;
commands.clear();
for (const auto& packet : {
makePacket(0x20, {0x31100, low(&flag[0]), high(&flag[0])}),
makePacket(0x37, {0x100, low(&written[2]), high(&written[2]), 5}, 1),
call,
makePacket(0x20, {0x31000, low(&flag[0]), high(&flag[0])}),
makePacket(0x37, {0x100, low(&written[3]), high(&written[3]), 6}, 1),
makePacket(0x20, {0, 0, 0})
}) commands.insert(commands.end(), packet.begin(), packet.end());
submit(commands);
check(written[2] == 5, "packet with a true predicate was skipped");
check(nestedWritten[0] == 3, "command buffer with a true predicate was skipped");
check(written[3] == 0, "draw-not-visible packet ran with a set value");
auto chain = makePacket(0x3f, {low(nested.data()), high(nested.data()), 0x0f300000u | static_cast<std::uint32_t>(nested.size())}, 1);
Packet chained{chain.data(), static_cast<std::uint32_t>(chain.size()), 0, {}};
expectFailure([&] { sceAgcDriverSubmitDcb(&chained); }, "predicated command buffer chains");
AgcDriverWaitIdle_nid_postfix();
}
std::vector<std::uint32_t> condWrite(std::uint32_t control, const std::uint32_t& poll, std::uint32_t reference, std::uint32_t mask, std::uint32_t& target, std::uint32_t value) {
return makePacket(0x45, {control, low(&poll), high(&poll), reference, mask, low(&target), high(&target), value});
}
void testConditionalWrite() {
AgcDriver::QueueState state;
alignas(4) std::uint32_t poll = 0x1234u;
alignas(4) std::uint32_t target = 0;
const auto valid = condWrite(0x113, poll, 0x34, 0xff, target, 7);
check(AgcDriver::Pm4::Name(valid[0]) == "COND_WRITE" && valid[0] == 0xc0074500u, "COND_WRITE header or name mismatch");
check(AgcDriver::Pm4::UnsupportedReason(valid[0]).empty() && AgcDriver::Pm4::AccessesMemory(valid[0]), "COND_WRITE is rejected or does not synchronize guest memory");
const std::array<std::pair<std::uint32_t, bool>, 7> functions{{{0, true}, {1, false}, {2, true}, {3, true}, {4, false}, {5, true}, {6, false}}};
for (const auto& [function, writes] : functions) {
target = 0;
execute(state, condWrite(0x110 | function, poll, 0x34, 0xff, target, 9));
check(target == (writes ? 9u : 0u), "COND_WRITE compared the masked poll value incorrectly");
}
target = 0;
execute(state, condWrite(0x111, poll, 0x35, 0xff, target, 10));
check(target == 10, "COND_WRITE less-than did not write");
target = 0;
execute(state, condWrite(0x116, poll, 0x1233, 0xffffffffu, target, 11));
check(target == 11, "COND_WRITE greater-than with a full mask did not write");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x45, {0x113, low(&poll), high(&poll), 0, 0, low(&target), high(&target)}), 0); }, "packet size");
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x2000113, poll, 0, 0, target, 0), 0); }, "reserved");
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x103, poll, 0, 0, target, 0), 0); }, "register-space");
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x117, poll, 0, 0, target, 0), 0); }, "compare function");
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x013, poll, 0, 0, target, 0), 0); }, "destination");
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x213, poll, 0, 0, target, 0), 0); }, "destination");
auto misaligned = condWrite(0x113, poll, 0, 0, target, 0);
misaligned[2] += 2;
expectFailure([&] { AgcDriver::Pm4::Validate(misaligned, 0); }, "misaligned");
misaligned = condWrite(0x113, poll, 0, 0, target, 0);
misaligned[6] += 1;
expectFailure([&] { AgcDriver::Pm4::Validate(misaligned, 0); }, "misaligned");
struct MemoryState {
std::uint32_t poll = 0;
std::uint32_t target = 0;
bool read = false;
bool written = false;
};
static MemoryState memory;
memory = {};
struct FlushHookReset {
~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); }
} reset;
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) {
check(bytes == sizeof(std::uint32_t), "COND_WRITE resolved an unrelated range");
if (address == reinterpret_cast<std::uintptr_t>(&memory.target)) {
memory.written = true;
} else {
check(address == reinterpret_cast<std::uintptr_t>(&memory.poll), "COND_WRITE resolved an unrelated poll address");
memory.poll = 5;
memory.read = true;
}
});
execute(state, condWrite(0x113, memory.poll, 5, 0xffffffffu, memory.target, 12));
check(memory.read && memory.written && memory.target == 12, "COND_WRITE used a stale poll value or skipped synchronizing its target");
}
void testConditionalWriteSubmission() {
alignas(4) static std::uint32_t poll = 0;
static std::array<std::uint32_t, 2> results{};
results.fill(0);
poll = 0;
auto words = joinPackets({writeWord(poll, 5), condWrite(0x113, poll, 5, 0xffffffffu, results[0], 51), condWrite(0x114, poll, 5, 0xffffffffu, results[1], 52)});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(results[0] == 51, "COND_WRITE read its poll value before an earlier packet of its queue stored it");
check(results[1] == 0, "COND_WRITE wrote although its comparison failed");
}
void testAsyncMemoryFailure() {
auto commands = makePacket(0x37, {0x100, 0x1000, 0, 1});
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "memory packet was not submitted");
expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "guest");
expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }, "guest");
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }, "guest");
expectFailure([] { LibcRunShutdown_nid_postfix(); }, "guest");
}
class CapturedStderr {
public:
CapturedStderr() {
#ifdef _WIN32
path = std::filesystem::temp_directory_path() / ("aps5-pm4-ring-" + std::to_string(GetCurrentProcessId()) + ".txt");
file = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
check(file != INVALID_HANDLE_VALUE, "cannot create the stderr capture file");
previous = GetStdHandle(STD_ERROR_HANDLE);
check(SetStdHandle(STD_ERROR_HANDLE, file) != 0, "cannot redirect stderr");
#else
path = std::filesystem::temp_directory_path() / ("aps5-pm4-ring-" + std::to_string(::getpid()) + ".txt");
file = ::open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0600);
check(file >= 0, "cannot create the stderr capture file");
std::fflush(stderr);
previous = ::dup(2);
check(previous >= 0 && ::dup2(file, 2) >= 0, "cannot redirect stderr");
#endif
}
~CapturedStderr() {
#ifdef _WIN32
SetStdHandle(STD_ERROR_HANDLE, previous);
CloseHandle(file);
#else
std::fflush(stderr);
::dup2(previous, 2);
::close(previous);
::close(file);
#endif
std::error_code ignored;
std::filesystem::remove(path, ignored);
}
CapturedStderr(const CapturedStderr&) = delete;
CapturedStderr& operator=(const CapturedStderr&) = delete;
std::vector<std::string> RingLines() const {
std::ifstream input(path, std::ios::binary);
std::vector<std::string> lines;
for (std::string line; std::getline(input, line);) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.starts_with("[pm4 ring]")) lines.push_back(line);
}
return lines;
}
private:
std::filesystem::path path;
#ifdef _WIN32
HANDLE file = INVALID_HANDLE_VALUE;
HANDLE previous = nullptr;
#else
int file = -1;
int previous = -1;
#endif
};
std::vector<std::string> recordedWrites(const std::vector<std::string>& lines) {
std::vector<std::string> writes;
std::string entry;
for (const auto& line : lines) {
if (line.starts_with("[pm4 ring] queue ") && line.find(" #") != std::string::npos) {
entry = line.substr(17, line.find(" serial") - 17);
} else if (line.find("WRITE_DATA") != std::string::npos) {
check(!entry.empty() && line.size() >= 8, "a WRITE_DATA line is not under a submission");
writes.push_back(entry + " " + line.substr(line.size() - 8));
entry.clear();
}
}
return writes;
}
void submitWrites(std::uint32_t queue, std::uint32_t first, std::uint32_t count) {
alignas(8) static std::uint32_t target = 0;
for (std::uint32_t value = first; value < first + count; ++value) {
auto words = writeWord(target, value);
submitWords(words, queue);
}
AgcDriverWaitIdle_nid_postfix();
}
void testSubmissionRing() {
submitWrites(0, 0x5a000001u, 6);
submitWrites(0x20, 0x5b000001u, 2);
std::vector<std::string> lines;
{
CapturedStderr capture;
AgcDriver::DriverDetail::DumpSubmissionRings("ring test");
lines = capture.RingLines();
}
check(!lines.empty() && lines.front() == "[pm4 ring] ring test: 2 queue(s), up to 4 submissions each, oldest first", "the ring dump header is wrong");
check(std::find(lines.begin(), lines.end(), "[pm4 ring] queue 0x0: 6 submissions recorded, last 4 kept") != lines.end(), "the ring dump does not count every submission of queue 0");
check(std::find(lines.begin(), lines.end(), "[pm4 ring] queue 0x20: 2 submissions recorded, last 2 kept") != lines.end(), "the ring dump does not count every submission of queue 0x20");
const std::vector<std::string> expected{"0x0 #3 5a000003", "0x0 #4 5a000004", "0x0 #5 5a000005", "0x0 #6 5a000006", "0x20 #1 5b000001", "0x20 #2 5b000002"};
check(recordedWrites(lines) == expected, "the ring dump does not hold exactly the last submissions of each queue in order");
}
void testEmptySubmissionRing() {
CapturedStderr capture;
AgcDriver::DriverDetail::DumpSubmissionRings("ring test");
check(capture.RingLines() == std::vector<std::string>{"[pm4 ring] ring test: no submissions recorded"}, "the dump of empty rings is not one line");
}
void testDisabledSubmissionRing() {
submitWrites(0, 0x5a000001u, 2);
CapturedStderr capture;
AgcDriver::DriverDetail::DumpSubmissionRings("ring test");
check(capture.RingLines() == std::vector<std::string>{"[pm4 ring] ring test: disabled (APS5_PM4_RING=0)"}, "the dump of disabled rings is not one line");
}
void expectSettingRejected(const char* text) {
alignas(8) static std::uint32_t target = 0;
auto words = writeWord(target, 1);
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }, text);
AgcDriverWaitIdle_nid_postfix();
check(target == 0, "a submission rejected for its ring or watchdog setting was executed");
}
void testInvalidSubmissionRing() {
expectSettingRejected("APS5_PM4_RING must be a number");
}
void testInvalidStallWatchdog() {
expectSettingRejected("APS5_STALL_WATCHDOG_SECS must be a number");
}
void testSubmissionRingFailureReport() {
submitWrites(0, 0x5a000001u, 3);
std::vector<std::string> lines;
{
CapturedStderr capture;
LibcRunFailureReports_nid_no_patch();
lines = capture.RingLines();
}
check(!lines.empty() && lines.front() == "[pm4 ring] failure report: 1 queue(s), up to 2 submissions each, oldest first", "the failure report does not dump the rings");
check(recordedWrites(lines) == std::vector<std::string>{"0x0 #2 5a000002", "0x0 #3 5a000003"}, "the failure report does not hold the last submissions in order");
}
#ifdef _WIN32
std::atomic<DWORD> heapTrapThread{0};
std::atomic<std::uint32_t> heapTrapAllocations{0};
decltype(&HeapAlloc) heapAllocOriginal = nullptr;
decltype(&HeapReAlloc) heapReAllocOriginal = nullptr;
LPVOID WINAPI trappedHeapAlloc(HANDLE heap, DWORD flags, SIZE_T bytes) {
if (GetCurrentThreadId() == heapTrapThread.load()) heapTrapAllocations.fetch_add(1);
return heapAllocOriginal(heap, flags, bytes);
}
LPVOID WINAPI trappedHeapReAlloc(HANDLE heap, DWORD flags, LPVOID block, SIZE_T bytes) {
if (GetCurrentThreadId() == heapTrapThread.load()) heapTrapAllocations.fetch_add(1);
return heapReAllocOriginal(heap, flags, block, bytes);
}
void** importSlot(HMODULE module, const char* function) {
auto* base = reinterpret_cast<std::byte*>(module);
const auto* headers = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + reinterpret_cast<const IMAGE_DOS_HEADER*>(base)->e_lfanew);
const auto& directory = headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
for (auto* import = reinterpret_cast<const IMAGE_IMPORT_DESCRIPTOR*>(base + directory.VirtualAddress); import->Name != 0; ++import) {
const auto* names = reinterpret_cast<const IMAGE_THUNK_DATA*>(base + import->OriginalFirstThunk);
auto* slots = reinterpret_cast<IMAGE_THUNK_DATA*>(base + import->FirstThunk);
for (; names->u1.AddressOfData != 0; ++names, ++slots) {
if (IMAGE_SNAP_BY_ORDINAL(names->u1.Ordinal)) continue;
const auto* byName = reinterpret_cast<const IMAGE_IMPORT_BY_NAME*>(base + names->u1.AddressOfData);
if (std::strcmp(reinterpret_cast<const char*>(byName->Name), function) == 0) return reinterpret_cast<void**>(&slots->u1.Function);
}
}
return nullptr;
}
void replaceSlot(void** slot, void* value) {
DWORD protection = 0;
check(VirtualProtect(slot, sizeof(void*), PAGE_READWRITE, &protection) != 0, "cannot unprotect a ucrtbase.dll import");
*slot = value;
DWORD ignored = 0;
check(VirtualProtect(slot, sizeof(void*), protection, &ignored) != 0, "cannot protect a ucrtbase.dll import");
}
class CrtHeapTrap {
public:
CrtHeapTrap() {
const HMODULE runtime = GetModuleHandleW(L"ucrtbase.dll");
check(runtime != nullptr, "ucrtbase.dll is not loaded");
allocSlot = importSlot(runtime, "HeapAlloc");
reAllocSlot = importSlot(runtime, "HeapReAlloc");
check(allocSlot != nullptr && reAllocSlot != nullptr, "ucrtbase.dll does not import HeapAlloc and HeapReAlloc");
heapAllocOriginal = reinterpret_cast<decltype(&HeapAlloc)>(*allocSlot);
heapReAllocOriginal = reinterpret_cast<decltype(&HeapReAlloc)>(*reAllocSlot);
replaceSlot(allocSlot, reinterpret_cast<void*>(trappedHeapAlloc));
replaceSlot(reAllocSlot, reinterpret_cast<void*>(trappedHeapReAlloc));
}
~CrtHeapTrap() {
replaceSlot(allocSlot, reinterpret_cast<void*>(heapAllocOriginal));
replaceSlot(reAllocSlot, reinterpret_cast<void*>(heapReAllocOriginal));
}
CrtHeapTrap(const CrtHeapTrap&) = delete;
CrtHeapTrap& operator=(const CrtHeapTrap&) = delete;
template<typename TAction>
std::uint32_t Allocations(TAction action) {
heapTrapAllocations.store(0);
heapTrapThread.store(GetCurrentThreadId());
action();
heapTrapThread.store(0);
return heapTrapAllocations.load();
}
private:
void** allocSlot = nullptr;
void** reAllocSlot = nullptr;
};
void testSubmissionRingFailureReportHeap() {
alignas(8) static std::uint32_t target = 0;
auto words = joinPackets({makePacket(0x13, {0x40}), writeWord(target, 0x5c000001u)});
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
std::vector<std::string> lines;
{
CrtHeapTrap trap;
check(trap.Allocations([&] { check(AgcDriver::Pm4::Name(words[0]) == "INDEX_BUFFER_SIZE", "INDEX_BUFFER_SIZE is misnamed"); }) != 0, "the heap trap does not see the allocation of a packet name");
CapturedStderr capture;
const auto allocations = trap.Allocations([] { LibcRunFailureReports_nid_no_patch(); });
lines = capture.RingLines();
check(allocations == 0, "the failure report allocated from the C runtime heap");
}
check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 INDEX_BUFFER_SIZE 00000040") != std::string::npos; }), "the failure report does not name INDEX_BUFFER_SIZE");
check(recordedWrites(lines) == std::vector<std::string>{"0x0 #1 5c000001"}, "the failure report does not hold the submission");
}
#endif
bool hasRingLine(const std::vector<std::string>& lines, std::string_view prefix, std::string_view suffix) {
return std::any_of(lines.begin(), lines.end(), [&](const std::string& line) { return line.size() >= prefix.size() + suffix.size() && line.starts_with(prefix) && line.ends_with(suffix); });
}
void awaitRingLine(const CapturedStderr& capture, std::string_view prefix, std::string_view suffix, const char* reason) {
for (int waited = 0; waited < 1000 && !hasRingLine(capture.RingLines(), prefix, suffix); ++waited) std::this_thread::sleep_for(std::chrono::milliseconds(10));
check(hasRingLine(capture.RingLines(), prefix, suffix), reason);
}
void checkOneStallDump(const std::vector<std::string>& lines, std::uint32_t& done) {
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "the stalled submission did not finish after it was released");
check(std::count_if(lines.begin(), lines.end(), [](const std::string& line) { return line.find("has not progressed") != std::string::npos; }) == 1, "a stalled waiter dumped the rings more than once");
}
std::string stallPrefix(const char* waiter, const void* address) {
char text[160];
std::snprintf(text, sizeof(text), "[pm4 ring] queue 0x0 %s 0x%llx has not progressed", waiter, static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(address)));
return text;
}
void testStallWatchdog() {
alignas(8) static std::uint32_t gate = 0;
alignas(8) static std::uint32_t done = 0;
std::vector<std::string> lines;
{
CapturedStderr capture;
auto words = joinPackets({makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), writeWord(done, 1)});
submitWords(words);
awaitRingLine(capture, stallPrefix("WAIT_REG_MEM at", &gate), " for 1 s (APS5_STALL_WATCHDOG_SECS): 1 queue(s), up to 16 submissions each, oldest first",
"a WAIT_REG_MEM stalled past APS5_STALL_WATCHDOG_SECS did not dump the rings");
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
check(std::atomic_ref<std::uint32_t>(done).load() == 0, "the stalled submission finished before its gate was opened");
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
lines = capture.RingLines();
}
checkOneStallDump(lines, done);
check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 WAIT_REG_MEM 00000013") != std::string::npos; }), "the stall dump does not decode the stalled submission");
}
void testStallWatchdogRewind() {
alignas(8) static std::uint32_t done = 0;
static std::vector<std::uint32_t> words;
words = joinPackets({makePacket(0x59, {0}), writeWord(done, 1)});
std::vector<std::string> lines;
{
CapturedStderr capture;
submitWords(words);
awaitRingLine(capture, stallPrefix("REWIND at", &words[1]), " for 1 s (APS5_STALL_WATCHDOG_SECS): 1 queue(s), up to 16 submissions each, oldest first",
"a REWIND stalled past APS5_STALL_WATCHDOG_SECS did not dump the rings");
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
check(std::atomic_ref<std::uint32_t>(done).load() == 0, "the tail of a REWIND ran before the title released it");
std::atomic_ref<std::uint32_t>(words[1]).store(0x80000000u);
AgcDriverWaitIdle_nid_postfix();
lines = capture.RingLines();
}
checkOneStallDump(lines, done);
check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 REWIND 00000000") != std::string::npos; }), "the stall dump does not decode the stalled REWIND");
}
struct RenderingGateState {
std::mutex mutex;
std::condition_variable changed;
bool closed = true;
bool waiting = false;
void Open() {
{
std::lock_guard lock(mutex);
closed = false;
}
changed.notify_all();
}
};
class RenderingGate final : public AgcDriver::IRenderingWait {
public:
explicit RenderingGate(std::shared_ptr<RenderingGateState> state) : state(std::move(state)) {}
void Wait() override {
std::unique_lock lock(state->mutex);
state->waiting = true;
state->changed.notify_all();
state->changed.wait(lock, [&] { return !state->closed; });
}
private:
std::shared_ptr<RenderingGateState> state;
};
class GatedOutput final : public AgcDriver::IVideoOutput {
public:
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { throw std::runtime_error("the gated test output takes no flips"); }
std::shared_ptr<AgcDriver::IRenderingWait> CaptureRenderingWait(std::uint32_t) override { return std::make_shared<RenderingGate>(state); }
void Fail(std::exception_ptr) noexcept override {}
std::shared_ptr<RenderingGateState> state = std::make_shared<RenderingGateState>();
};
void testStallWatchdogOrder() {
alignas(8) static std::uint32_t done = 0;
auto output = std::make_shared<GatedOutput>();
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
struct GateRelease {
std::shared_ptr<RenderingGateState> state;
~GateRelease() { state->Open(); }
} release{output->state};
std::vector<std::string> lines;
{
CapturedStderr capture;
std::vector<std::uint32_t> wait{AgcDriver::RenderingWaitPacketHeader, 7, 0, 0};
submitWords(wait);
{
std::unique_lock lock(output->state->mutex);
check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "queue 0 did not reach the rendering wait");
}
auto words = writeWord(done, 1);
submitWords(words, 0x20);
awaitRingLine(capture, "[pm4 ring] queue 0x20 order behind queue 0 for event 0x", " has not progressed for 1 s (APS5_STALL_WATCHDOG_SECS): 2 queue(s), up to 16 submissions each, oldest first",
"a compute submission held behind queue 0 past APS5_STALL_WATCHDOG_SECS did not dump the rings");
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
check(std::atomic_ref<std::uint32_t>(done).load() == 0, "a compute submission passed queue 0 while it was held");
output->state->Open();
AgcDriverWaitIdle_nid_postfix();
lines = capture.RingLines();
}
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
checkOneStallDump(lines, done);
}
void testRefusedWorkerPriority() {
alignas(8) static std::uint32_t result = 0;
auto words = writeWord(result, 91);
submitWords(words);
expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "refused");
check(result == 0, "a queue worker whose priority was refused executed its submission");
expectFailure([] { LibcRunShutdown_nid_postfix(); }, "refused");
}
}
void testUnwrittenUserData() {
AgcDriver::Registers shader{{0x8c, 0x100}, {0x8d, 0}, {0x240, 0x200}};
check(AgcDriver::DriverDetail::readUserData(shader, 0x8c) == 0x100 && AgcDriver::DriverDetail::readUserData(shader, 0x240) == 0x200, "a written user data register was not read");
check(AgcDriver::DriverDetail::readUserData(shader, 0x8d) == 0, "a user data register written as zero was not read");
check(AgcDriver::DriverDetail::readUserData(shader, 0x95) == 0 && AgcDriver::DriverDetail::readUserData(shader, 0x241) == 0, "an unwritten user data register does not read zero");
expectFailure([&] { static_cast<void>(AgcDriver::DriverDetail::readRegister(shader, 0x95)); }, "required shader register");
}
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string(argv[1]) == "failure") {
testAsyncMemoryFailure();
std::puts("PM4 asynchronous memory failure propagated to idle, suspend, submit and shutdown");
return 0;
}
if (argc == 2 && std::string(argv[1]) == "priority-refused") {
testRefusedWorkerPriority();
std::puts("PM4 refused queue worker priority propagated to idle and shutdown");
return 0;
}
const std::pair<const char*, void (*)()> ringModes[]{
{"ring", testSubmissionRing}, {"ring-empty", testEmptySubmissionRing}, {"ring-disabled", testDisabledSubmissionRing},
{"ring-invalid", testInvalidSubmissionRing}, {"ring-report", testSubmissionRingFailureReport}, {"stall-watchdog", testStallWatchdog},
{"stall-watchdog-invalid", testInvalidStallWatchdog}, {"stall-watchdog-rewind", testStallWatchdogRewind}, {"stall-watchdog-order", testStallWatchdogOrder}};
if (argc == 2 && std::string(argv[1]) == "ring-report-heap") {
#ifdef _WIN32
testSubmissionRingFailureReportHeap();
LibcRunShutdown_nid_postfix();
std::puts("PM4 ring-report-heap test passed");
return 0;
#else
std::puts("the C runtime heap trap needs ucrtbase.dll");
return 77;
#endif
}
for (const auto& [mode, test] : ringModes) {
if (argc != 2 || std::string(argv[1]) != mode) continue;
test();
LibcRunShutdown_nid_postfix();
std::printf("PM4 %s test passed\n", mode);
return 0;
}
testCatalog();
testNopPad();
testWriteChangedKeepsUntouchedBytes();
testWriteChangedStoresExactlyTheChangedRuns();
testRegisters();
testRegisterFile();
testContextAndBases();
testIndexedDraw();
testAutoDraw();
testIndirectDraw();
testIndexAttributesIndirect();
testMemory();
testTraceRegisterWrites();
testCopies();
testMemoryCopyDecode();
testMemorySynchronization();
testConditionalValidation();
testConditionReadSynchronization();
testEventWrite();
testAcquireMem();
testConditionalWrite();
testConditionalWriteSubmission();
testPredication();
testUnwrittenUserData();
testDriverSubmission();
testRegisterListsReadAtSubmission();
testIndexAttributesReadAtExecution();
testPredicatedSubmission();
testConditionalSubmission();
testSuspendPointWritesQueuedLabels();
testNopPadSubmission();
testBranchSubmission();
LibcRunShutdown_nid_postfix();
std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { LibcRunShutdown_nid_postfix(); } catch (...) {}
return 1;
}
}