Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp
T
Longziip ae67e2b7f9 fix(agc): write the reread indirect register list when skipping its invalid entries
When the list read at submission is invalid, the worker now always
executes the copy read again where the packet runs. With
APS5_SKIP_INVALID_INDIRECT_REGISTERS=1 only that copy's invalid entries
are skipped, so the valid entries of a partly updated list are written
instead of the stale ones from submission.

testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries submits a list
that is invalid at submission, updates it to a valid entry plus an
invalid one before its packet runs, and reads the register back with
COPY_DATA.
2026-10-19 17:29:53 +01:00

1927 lines
120 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/Draw/DrawPipeline.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 <cstdlib>
#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;
check(state.context.contains(0x280) && state.context.at(0x280) == 0x00080008u && state.context.contains(0x281) && state.context.at(0x281) == 0xffff0000u, "initial point context differs from the SDK defaults");
execute(state, makePacket(0x69, {0x280, 0x00180018u, 0x00400008u}));
check(state.context.at(0x280) == 0x00180018u && state.context.at(0x281) == 0x00400008u, "explicit point size and limits were not stored");
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");
check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "CLEAR_STATE did not restore point size and limits");
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(0x69, {0x280, 0x00100010u, 0x00800010u}));
execute(state, makePacket(0x10, {2, 0}, 0x68));
check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "context pop did not restore saved point size and limits");
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);
AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x49}), 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x51}), 0); }, "dispatch modifiers");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x51}), 0); }, "indirect 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);
const auto largeIndirect = AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded);
check(largeIndirect.indexAddress == indexAddress && largeIndirect.indexCount == records[6] && largeIndirect.indexSize == 4, "an indirect draw did not preserve its declared index limit for record resolution");
loaded.indexBase = std::numeric_limits<std::uint64_t>::max() - 3u;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "index range size overflow");
loaded.indexBase = indexAddress + 1u;
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "misaligned index base");
loaded.indexBase = indexAddress;
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");
for (const auto eventType : {0x01u, 0x02u, 0x03u, 0x20u}) {
AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue");
for (std::uint32_t index = 1; index < 8; ++index) {
if (index == 3) continue;
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics event index");
}
for (const auto index : {3u, 8u, 9u, 10u, 11u}) {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType, 0x1000, 0x2}), 0); }, "stream-out statistics sampling to memory is not implemented");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics sampling to memory is not implemented");
}
for (const auto index : {12u, 13u, 14u, 15u}) {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "reserved bits");
}
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType, 0x1000, 0x2}), 0); }, "packet size");
}
}
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(0x46, {0x01}),
makePacket(0x46, {0x20}),
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 testInvalidRegisterListEntries() {
std::array<std::uint32_t, 6> pairs{0x038f038fu, 0xc8e9037eu, 0x10, 41, 0x11, 42};
const auto packet = makePacket(0x9f, {low(pairs.data()), high(pairs.data()), 0x80000000, 3});
check(!AgcDriver::Pm4::IndirectRegistersValid(pairs), "an offset dword with bits 16-27 set was taken as a register offset");
check(!AgcDriver::Pm4::IndirectRegistersValid(std::array<std::uint32_t, 2>{0xffffffffu, 0}), "the sentinel was taken as a register offset");
check(AgcDriver::Pm4::IndirectRegistersValid(std::array<std::uint32_t, 4>{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected");
AgcDriver::QueueState state;
expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register");
check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state");
AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true);
check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one");
}
void testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries() {
#ifdef _WIN32
check(_putenv_s("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1") == 0, "cannot enable skipping invalid register list entries");
#else
check(setenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1", 1) == 0, "cannot enable skipping invalid register list entries");
#endif
alignas(8) static std::uint32_t gate = 0;
alignas(8) static std::uint32_t copied = 0;
static std::array<std::uint32_t, 4> registers{0x038f038fu, 5, 0x11, 9};
auto words = joinPackets({
makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}),
makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 2}),
makePacket(0x40, {0x100, 0xa010, 0, low(&copied), high(&copied)})});
submitWords(words);
registers = {0x10, 77, 0x03900390u, 6};
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(copied).load() == 77, "a register list partly updated before its packet ran lost the current valid entries");
}
void testRegisterListReadAgainAtExecution() {
alignas(8) static std::uint32_t gate = 0;
static std::uint32_t done = 0;
static std::array<std::uint32_t, 2> registers{0x038f038fu, 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] = 0x10;
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "a register list invalid at submission and valid at execution was not read again");
}
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");
}
void testWaitRegMemMasking() {
alignas(8) static std::uint32_t mem32[4] = {0x12345678u, 0x9abcdef0u, 0x55555555u, 0xaaaaaaaau};
const auto base32 = reinterpret_cast<std::uintptr_t>(&mem32[0]);
for (std::uint32_t offset = 0; offset < 4; ++offset) {
const auto misaligned = base32 + offset;
const auto packet = makePacket(0x3c, {0x13, static_cast<std::uint32_t>(misaligned), static_cast<std::uint32_t>(misaligned >> 32u), 0x12345678u, 0xffffffffu, 0});
AgcDriver::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Validate(packet, 0x20);
const auto decoded = AgcDriver::Pm4::DecodeWait(packet);
check(decoded.address == base32, "WAIT_REG_MEM did not mask low 2 address bits");
check(decoded.bytes == 4, "WAIT_REG_MEM awaited bytes mismatch");
check(AgcDriver::Pm4::WaitSatisfied(packet), "WAIT_REG_MEM satisfied check failed with misaligned address");
check(AgcDriver::Pm4::WaitSatisfiedUnchecked(packet), "WAIT_REG_MEM satisfied unchecked failed with misaligned address");
}
alignas(8) static std::uint64_t mem64[2] = {0x0123456789abcdefull, 0xfedcba9876543210ull};
const auto base64 = reinterpret_cast<std::uintptr_t>(&mem64[0]);
for (std::uint32_t offset = 0; offset < 8; ++offset) {
const auto misaligned = base64 + offset;
const auto packet = makePacket(0x93, {0x13, static_cast<std::uint32_t>(misaligned), static_cast<std::uint32_t>(misaligned >> 32u), 0x89abcdefu, 0x01234567u, 0xffffffffu, 0xffffffffu, 0});
AgcDriver::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Validate(packet, 0x20);
const auto decoded = AgcDriver::Pm4::DecodeWait(packet);
check(decoded.address == base64, "WAIT_REG_MEM_64 did not mask low 3 address bits");
check(decoded.bytes == 8, "WAIT_REG_MEM_64 awaited bytes mismatch");
check(AgcDriver::Pm4::WaitSatisfied(packet), "WAIT_REG_MEM_64 satisfied check failed with misaligned address");
check(AgcDriver::Pm4::WaitSatisfiedUnchecked(packet), "WAIT_REG_MEM_64 satisfied unchecked failed with misaligned address");
}
const auto misaligned4 = base64 + 4;
for (std::uint32_t offset = 0; offset < 4; ++offset) {
const auto misaligned = misaligned4 + offset;
const auto packet = makePacket(0x93, {0x13, static_cast<std::uint32_t>(misaligned), static_cast<std::uint32_t>(misaligned >> 32u), 0x01234567u, 0, 0xffffffffu, 0, 0});
AgcDriver::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Validate(packet, 0x20);
const auto decoded = AgcDriver::Pm4::DecodeWait(packet);
check(decoded.address == misaligned4, "WAIT_REG_MEM_64 with zero high mask did not mask to 4-byte alignment");
check(decoded.bytes == 4, "WAIT_REG_MEM_64 with zero high mask expected 4 awaited bytes");
check(AgcDriver::Pm4::WaitSatisfied(packet), "WAIT_REG_MEM_64 zero high mask satisfied check failed");
check(AgcDriver::Pm4::WaitSatisfiedUnchecked(packet), "WAIT_REG_MEM_64 zero high mask unchecked check failed");
}
}
void waitDrawPipelineIdle() {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() != 0 && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void testWaitRegMemPipelineDrain() {
alignas(8) static std::uint32_t satisfiedTarget = 42;
alignas(8) static std::uint32_t pendingTarget = 0;
const auto pendingAddr = reinterpret_cast<std::uintptr_t>(&pendingTarget);
std::atomic<bool> blockerHold{true};
std::atomic<bool> blockerStarted{false};
AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] {
blockerStarted.store(true);
while (blockerHold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
}, { {pendingAddr, pendingAddr + 4} });
while (!blockerStarted.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 1, "draw pipeline should have 1 item");
auto nonOverlappingWords = makePacket(0x3c, {0x13, low(&satisfiedTarget), high(&satisfiedTarget), 42, 0xffffffffu, 0});
submitWords(nonOverlappingWords, 0);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() >= 2, "non-overlapping wait did not elide pipeline drain");
blockerHold.store(false);
AgcDriverWaitIdle_nid_postfix();
waitDrawPipelineIdle();
check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 0, "draw pipeline should be idle");
std::atomic<bool> blocker2Hold{true};
std::atomic<bool> blocker2Started{false};
AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] {
blocker2Started.store(true);
while (blocker2Hold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
}, { {pendingAddr, pendingAddr + 4} });
while (!blocker2Started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
auto overlappingWords = makePacket(0x3c, {0x13, low(&pendingTarget), high(&pendingTarget), 99, 0xffffffffu, 0});
submitWords(overlappingWords, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 1, "overlapping wait proceeded past drain while pipeline held");
std::atomic_ref<std::uint32_t>(pendingTarget).store(99);
blocker2Hold.store(false);
AgcDriverWaitIdle_nid_postfix();
waitDrawPipelineIdle();
check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 0, "pipeline should be idle after overlapping wait drain");
}
void testWaitRegMem64ZeroHighMaskDrain() {
alignas(8) static std::uint32_t qword[2] = {42, 0};
const auto base = reinterpret_cast<std::uintptr_t>(&qword[0]);
const auto packetAddr = base + 4;
std::atomic<bool> hold{true};
std::atomic<bool> started{false};
AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] {
started.store(true);
while (hold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
}, { {base, base + 4} });
while (!started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
auto words = makePacket(0x93, {0x13, static_cast<std::uint32_t>(packetAddr), static_cast<std::uint32_t>(packetAddr >> 32u), 42, 0, 0xffffffffu, 0, 0});
submitWords(words, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const auto outstanding = AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding();
std::atomic_ref<std::uint32_t>(qword[1]).store(42);
hold.store(false);
AgcDriverWaitIdle_nid_postfix();
check(outstanding == 1, "WAIT_REG_MEM_64 with zero high mask at 4 mod 8 skipped the drain of a pending write to the dword it compares");
}
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();
testInvalidRegisterListEntries();
testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries();
testRegisterListReadAgainAtExecution();
testIndexAttributesReadAtExecution();
testPredicatedSubmission();
testConditionalSubmission();
testSuspendPointWritesQueuedLabels();
testNopPadSubmission();
testBranchSubmission();
testWaitRegMemMasking();
testWaitRegMemPipelineDrain();
testWaitRegMem64ZeroHighMaskDrain();
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;
}
}