feat(agcDriver): add 54 PM4 execution and indirect dispatch support

This commit is contained in:
boykopovar 2026-09-14 11:43:13 +03:00
parent 990ae6a0ff
commit 699df8d5fa
10 changed files with 785 additions and 110 deletions

View File

@ -1,5 +1,7 @@
add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp
@ -39,3 +41,9 @@ target_include_directories(agc_driver_flip_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_driver_flip_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_driver_flip_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_flip_tests)
add_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL tests/Pm4.cpp)
target_include_directories(agc_driver_pm4_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_driver_pm4_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_driver_pm4_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_pm4_tests)

View File

@ -0,0 +1,16 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_GUESTMEMORY_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_GUESTMEMORY_HPP
#include <cstddef>
#include <cstdint>
#include <span>
namespace AgcDriver::GuestMemory {
void CheckRange(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable = false);
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment = 1);
void Write(std::uint64_t address, std::span<const std::byte> source, std::size_t alignment = 1);
}
#endif

View File

@ -0,0 +1,21 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PM4_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PM4_HPP
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp"
#include <array>
#include <span>
#include <string>
namespace AgcDriver::Pm4 {
std::string Name(std::uint32_t header);
std::string_view UnsupportedReason(std::uint32_t header);
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
void Execute(std::span<const std::uint32_t> packet, QueueState& queue);
bool AccessesMemory(std::uint32_t header);
std::array<std::uint32_t, 5> ResolveDispatch(std::span<const std::uint32_t> packet, const QueueState& queue);
}
#endif

View File

@ -0,0 +1,73 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PM4OPCODES_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PM4OPCODES_HPP
#include <cstdint>
#include <string_view>
namespace AgcDriver::Pm4 {
struct Opcode {
std::uint32_t value;
std::string_view name;
};
inline constexpr Opcode Opcodes[] = {
{0x10, "NOP"},
{0x11, "SET_BASE"},
{0x12, "CLEAR_STATE"},
{0x13, "INDEX_BUFFER_SIZE"},
{0x15, "DISPATCH_DIRECT"},
{0x16, "DISPATCH_INDIRECT"},
{0x20, "SET_PREDICATION"},
{0x22, "COND_EXEC"},
{0x24, "DRAW_INDIRECT"},
{0x25, "DRAW_INDEX_INDIRECT"},
{0x26, "INDEX_BASE"},
{0x27, "DRAW_INDEX_2"},
{0x28, "CONTEXT_CONTROL"},
{0x2A, "INDEX_TYPE"},
{0x2C, "DRAW_INDIRECT_MULTI"},
{0x2D, "DRAW_INDEX_AUTO"},
{0x2F, "NUM_INSTANCES"},
{0x33, "INDIRECT_BUFFER_CNST"},
{0x35, "DRAW_INDEX_OFFSET_2"},
{0x37, "WRITE_DATA"},
{0x39, "MEM_SEMAPHORE"},
{0x38, "DRAW_INDEX_INDIRECT_MULTI"},
{0x3A, "DISPATCH_DRAW_PREAMBLE"},
{0x3C, "WAIT_REG_MEM"},
{0x3F, "INDIRECT_BUFFER"},
{0x40, "COPY_DATA"},
{0x41, "CP_DMA"},
{0x42, "PFP_SYNC_ME"},
{0x43, "SURFACE_SYNC"},
{0x46, "EVENT_WRITE"},
{0x47, "EVENT_WRITE_EOP"},
{0x48, "EVENT_WRITE_EOS"},
{0x49, "RELEASE_MEM"},
{0x50, "DMA_DATA"},
{0x58, "ACQUIRE_MEM"},
{0x59, "REWIND"},
{0x63, "SET_SH_REG_INDIRECT"},
{0x64, "SET_UCONFIG_REG_INDIRECT"},
{0x68, "SET_CONFIG_REG"},
{0x69, "SET_CONTEXT_REG"},
{0x76, "SET_SH_REG"},
{0x78, "SET_QUEUE_REG"},
{0x79, "SET_UCONFIG_REG"},
{0x7A, "SET_UCONFIG_REG_INDEX"},
{0x81, "WRITE_CONST_RAM"},
{0x83, "DUMP_CONST_RAM"},
{0x84, "INCREMENT_CE_COUNTER"},
{0x85, "INCREMENT_DE_COUNTER"},
{0x86, "WAIT_ON_CE_COUNTER"},
{0x88, "WAIT_ON_DE_COUNTER_DIFF"},
{0x8D, "DISPATCH_DRAW"},
{0x8E, "GET_LOD_STATS"},
{0x93, "WAIT_REG_MEM_64"},
{0x9F, "SET_CONTEXT_REG_INDIRECT"},
};
}
#endif

View File

@ -3,6 +3,10 @@
#include <cstdint>
#include <map>
#include <array>
#include <optional>
#include <string>
#include <vector>
namespace AgcDriver {
@ -12,6 +16,15 @@ struct QueueState {
Registers shader;
Registers context;
Registers userConfig;
std::optional<Registers> savedContext;
std::array<std::uint32_t, 0x3000> constantRam{};
std::uint64_t indexBase = 0;
std::uint64_t drawIndirectBase = 0;
std::uint64_t dispatchIndirectBase = 0;
std::uint32_t indexBufferSize = 0;
std::uint32_t indexType = 0;
std::uint32_t instanceCount = 1;
std::vector<std::string> markers;
void ClearContext() {
context.clear();

View File

@ -1,4 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
@ -18,15 +20,6 @@
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <fstream>
#include <sstream>
#endif
namespace AgcDriver {
namespace {
@ -37,42 +30,6 @@ void require(bool condition, const char* reason) {
}
}
void checkRange(const void* pointer, std::size_t bytes, std::size_t alignment) {
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
require(address != 0 && address % alignment == 0, "null or misaligned address");
require(bytes <= std::numeric_limits<std::uintptr_t>::max() - address, "address range overflow");
auto cursor = address;
const auto end = address + bytes;
#ifdef _WIN32
while (cursor < end) {
MEMORY_BASIC_INFORMATION memory{};
require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query guest memory");
require(memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0, "guest memory is not readable");
const auto protection = memory.Protect & 0xffu;
require(protection == PAGE_READONLY || protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READ || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY, "guest memory has no read permission");
const auto base = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
require(memory.RegionSize <= std::numeric_limits<std::uintptr_t>::max() - base && base + memory.RegionSize > cursor, "invalid guest memory mapping");
cursor = std::min(end, base + memory.RegionSize);
}
#else
std::ifstream maps("/proc/self/maps");
require(maps.is_open(), "cannot query guest memory maps");
std::string line;
while (cursor < end && std::getline(maps, line)) {
std::istringstream fields(line);
std::uintptr_t first = 0;
std::uintptr_t last = 0;
char separator = 0;
std::string permissions;
require(static_cast<bool>(fields >> std::hex >> first >> separator >> last >> permissions) && separator == '-' && first < last && !permissions.empty(), "invalid guest memory map entry");
if (last <= cursor) continue;
require(first <= cursor && permissions[0] == 'r', "guest memory is not readable");
cursor = std::min(end, last);
}
require(cursor == end, "guest address range is not mapped");
#endif
}
struct ShaderSnapshot {
std::uint64_t codeAddress;
std::uint64_t headerAddress;
@ -138,14 +95,14 @@ public:
void Submit(const Packet* packet, std::uint32_t queue) {
CheckFailure();
require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue");
checkRange(packet, sizeof(Packet), alignof(Packet));
GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet));
const auto descriptor = *packet;
require(descriptor.flags == 0, "nonzero submission flags are not implemented");
Submission submission{};
submission.queue = queue;
if (descriptor.dw_num != 0) {
require(descriptor.dw_num <= std::numeric_limits<std::size_t>::max() / sizeof(std::uint32_t), "command size overflow");
checkRange(descriptor.addr, static_cast<std::size_t>(descriptor.dw_num) * sizeof(std::uint32_t), alignof(std::uint32_t));
GuestMemory::CheckRange(descriptor.addr, static_cast<std::size_t>(descriptor.dw_num) * sizeof(std::uint32_t), alignof(std::uint32_t));
submission.commands.assign(descriptor.addr, descriptor.addr + descriptor.dw_num);
}
validate(submission.commands, queue);
@ -264,13 +221,13 @@ public:
void RegisterShader(const Shader* shader) {
CheckFailure();
checkRange(shader, sizeof(Shader), alignof(Shader));
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
require(shader->file_header == 0x34333231u && shader->version == 0x18u, "invalid shader header");
require(shader->header_size >= sizeof(Shader), "shader header is smaller than its fixed fields");
require(shader->shader_size != 0 && (shader->shader_size & 3u) == 0, "invalid shader size");
checkRange(shader, shader->header_size, alignof(Shader));
GuestMemory::CheckRange(shader, shader->header_size, alignof(Shader));
const auto* code = const_cast<const void*>(shader->code);
checkRange(code, shader->shader_size, 256);
GuestMemory::CheckRange(code, shader->shader_size, 256);
ShaderSnapshot snapshot{reinterpret_cast<std::uintptr_t>(code), reinterpret_cast<std::uintptr_t>(shader), shader->type, {}, {}};
snapshot.code.resize(shader->shader_size / sizeof(std::uint32_t));
std::memcpy(snapshot.code.data(), code, shader->shader_size);
@ -320,36 +277,10 @@ private:
require((header & 0xc0000000u) == 0xc0000000u, "unsupported PM4 packet type");
const auto count = static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2;
require(count <= commands.size() - cursor, "truncated PM4 packet");
if (header == FlipPacketHeader) {
require(count == FlipPacketWords && queue == 0, "invalid flip packet size or queue");
cursor += count;
continue;
}
require((header & 0xffu) == 0, "PM4 header flags or internal extension are not implemented");
const auto packet = commands.subspan(cursor, count);
const auto opcode = (header >> 8u) & 0xffu;
switch (opcode) {
case 0x12:
require(queue == 0, "CLEAR_STATE in compute queue");
require(count == 2, "invalid CLEAR_STATE packet size");
require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits");
break;
case 0x10:
require((packet[1] & 0xffff0000u) != 0x68750000u, "marker NOP is not implemented");
break;
case 0x69:
case 0x76:
case 0x79:
require(count >= 3, "register packet has no values");
require(packet[1] <= 0xffffu && count - 2 <= 0x10000u - packet[1], "register range overflow or unsupported register index");
require(queue == 0 || opcode == 0x76, "graphics register packet in compute queue");
break;
case 0x15:
require(count == 5, "invalid dispatch packet size");
require((packet[4] & ~0x8000u) == 0x41u, "dispatch modifiers are not implemented");
break;
default:
throw std::runtime_error("AGC driver: unsupported PM4 opcode " + std::to_string(opcode) + " at DWORD " + std::to_string(cursor));
try {
Pm4::Validate(commands.subspan(cursor, count), queue);
} catch (const std::exception& error) {
throw std::runtime_error("AGC driver: " + Pm4::Name(header) + " at DWORD " + std::to_string(cursor) + ": " + error.what());
}
cursor += count;
}
@ -404,35 +335,21 @@ private:
const auto header = submission.commands[cursor];
const auto count = static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2;
const auto packet = std::span(submission.commands).subspan(cursor, count);
switch ((header >> 8u) & 0xffu) {
case 0x12:
queue.ClearContext();
break;
case 0x10:
if (header == FlipPacketHeader) {
{
std::lock_guard gpuLock(gpuMutex);
if (device != nullptr) device->WaitIdle();
}
CheckFailure();
submission.flips.at(cursor)->GpuReady();
}
break;
case 0x69:
case 0x76:
case 0x79: {
const auto opcode = (header >> 8u) & 0xffu;
auto& registers = opcode == 0x69 ? queue.context : opcode == 0x76 ? queue.shader : queue.userConfig;
for (std::size_t i = 2; i < count; ++i) {
registers.insert_or_assign(packet[1] + static_cast<std::uint32_t>(i - 2), packet[i]);
}
break;
}
case 0x15:
dispatch(queue, packet, submission);
break;
default:
throw std::runtime_error("AGC driver: validated packet has no executor");
const auto opcode = (header >> 8u) & 0xffu;
if (Pm4::AccessesMemory(header) || opcode == 0x42 || header == FlipPacketHeader) {
std::lock_guard gpuLock(gpuMutex);
if (device != nullptr) device->WaitIdle();
}
if (header == FlipPacketHeader) {
CheckFailure();
submission.flips.at(cursor)->GpuReady();
} else if (opcode == 0x15) {
dispatch(queue, packet, submission);
} else if (opcode == 0x16) {
const auto direct = Pm4::ResolveDispatch(packet, queue);
dispatch(queue, direct, submission);
} else if (opcode != 0x42) {
Pm4::Execute(packet, queue);
}
cursor += count;
}

View File

@ -0,0 +1,77 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <fstream>
#include <sstream>
#endif
namespace AgcDriver::GuestMemory {
namespace {
void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(std::string("AGC driver: ") + reason);
}
}
void CheckRange(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable) {
require(alignment != 0, "zero guest memory alignment");
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
require(address != 0 && address % alignment == 0, "null or misaligned address");
require(bytes <= std::numeric_limits<std::uintptr_t>::max() - address, "address range overflow");
auto cursor = address;
const auto end = address + bytes;
#ifdef _WIN32
while (cursor < end) {
MEMORY_BASIC_INFORMATION memory{};
require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query guest memory");
require(memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0, "guest memory is not readable");
const auto protection = memory.Protect & 0xffu;
require(protection == PAGE_READONLY || protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READ || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY, "guest memory has no read permission");
const auto base = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
require(memory.RegionSize <= std::numeric_limits<std::uintptr_t>::max() - base && base + memory.RegionSize > cursor, "invalid guest memory mapping");
require(!writable || protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY, "guest memory has no write permission");
cursor = std::min(end, base + memory.RegionSize);
}
#else
std::ifstream maps("/proc/self/maps");
require(maps.is_open(), "cannot query guest memory maps");
std::string line;
while (cursor < end && std::getline(maps, line)) {
std::istringstream fields(line);
std::uintptr_t first = 0;
std::uintptr_t last = 0;
char separator = 0;
std::string permissions;
require(static_cast<bool>(fields >> std::hex >> first >> separator >> last >> permissions) && separator == '-' && first < last && !permissions.empty(), "invalid guest memory map entry");
if (last <= cursor) continue;
require(first <= cursor && permissions[0] == 'r', "guest memory is not readable");
require(!writable || (permissions.size() > 1 && permissions[1] == 'w'), "guest memory has no write permission");
cursor = std::min(end, last);
}
require(cursor == end, "guest address range is not mapped");
#endif
}
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment) {
if (destination.empty()) return;
const auto* source = reinterpret_cast<const void*>(address);
CheckRange(source, destination.size(), alignment);
std::memcpy(destination.data(), source, destination.size());
}
void Write(std::uint64_t address, std::span<const std::byte> source, std::size_t alignment) {
if (source.empty()) return;
auto* destination = reinterpret_cast<void*>(address);
CheckRange(destination, source.size(), alignment, true);
std::memcpy(destination, source.data(), source.size());
}
}

View File

@ -0,0 +1,316 @@
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
namespace AgcDriver::Pm4 {
namespace {
void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
std::uint64_t address(std::uint32_t low, std::uint32_t high) {
return low | (static_cast<std::uint64_t>(high) << 32u);
}
std::uint32_t registerOffset(std::uint32_t value) {
require(value != 0xffffffffu, "indirect register sentinel semantics are not implemented");
const auto offset = value & ~0x70000000u;
require(offset <= 0xffffu, "extended register semantics are not implemented");
return offset;
}
Registers& registersFor(QueueState& queue, std::uint32_t opcode) {
if (opcode == 0x69 || opcode == 0x9f) return queue.context;
if (opcode == 0x76 || opcode == 0x63) return queue.shader;
return queue.userConfig;
}
void writeRegister(QueueState& queue, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) {
registersFor(queue, opcode).insert_or_assign(offset, value);
if ((opcode == 0x64 || opcode == 0x79 || opcode == 0x7a) && offset == 0x243) queue.indexType = value & 3u;
}
bool memorySelector(std::uint32_t selector) {
return selector == 0 || selector == 3;
}
std::uint32_t dmaSource(std::span<const std::uint32_t> packet) {
return ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u);
}
std::uint32_t dmaDestination(std::span<const std::uint32_t> packet) {
return ((packet[1] >> 20u) & 3u) | ((packet[6] >> 25u) & 4u) | ((packet[6] >> 26u) & 8u);
}
void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t bytes, bool immediate) {
if (bytes == 0) return;
GuestMemory::CheckRange(reinterpret_cast<void*>(destination), bytes, 1, true);
std::vector<std::byte> data(bytes);
if (immediate) {
const auto value = static_cast<std::uint32_t>(source);
for (std::size_t i = 0; i < bytes; ++i) data[i] = static_cast<std::byte>(value >> ((i % 4) * 8));
} else {
GuestMemory::Read(source, data);
}
GuestMemory::Write(destination, data);
}
}
std::string Name(std::uint32_t header) {
const auto opcode = (header >> 8u) & 0xffu;
if (opcode == 0x10 && (header & 0xfcu) != 0) {
switch ((header >> 2u) & 0x3fu) {
case 0x05: return "DRAW_RESET";
case 0x06: return "WAIT_FLIP_DONE";
case 0x09: return "DISPATCH_RESET";
case 0x0b: return "PUSH_MARKER";
case 0x0c: return "POP_MARKER";
case 0x14: return "ACQUIRE_MEM_CUSTOM";
case 0x15: return "WRITE_DATA_CUSTOM";
case 0x17: return "FLIP";
case 0x18: return "RELEASE_MEM_CUSTOM";
case 0x19: return "DMA_DATA_CUSTOM";
case 0x1a: return "CONTEXT_STATE";
default: return "UNKNOWN_CUSTOM";
}
}
for (const auto& entry : Opcodes) if (entry.value == opcode) return std::string(entry.name);
char text[24]{};
std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", opcode);
return text;
}
std::string_view UnsupportedReason(std::uint32_t header) {
const auto opcode = (header >> 8u) & 0xffu;
if (opcode == 0x10) {
switch ((header >> 2u) & 0x3fu) {
case 0: case 0x09: case 0x0b: case 0x0c: case 0x17: case 0x1a: return {};
case 0x06: return "VideoOut buffer-completion wait interface is not implemented";
case 0x14: case 0x18: return "guest cache actions and GPU release events are not implemented";
default: return "custom packet has no implemented contract in the reference dispatch table";
}
}
switch (opcode) {
case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x26:
case 0x2a: case 0x2f: case 0x37: case 0x40: case 0x42: case 0x50:
case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a:
case 0x81: case 0x83: case 0x9f: return {};
case 0x24: case 0x25: case 0x27: case 0x2c: case 0x2d: case 0x35: case 0x38: case 0x3a: case 0x8d:
return "graphics draw, shader stages and guest render-target materialization are not implemented";
case 0x20: return "GPU query predication is not implemented";
case 0x22: return "conditional command execution and conditional flip reservation are not implemented";
case 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented";
case 0x39: case 0x3c: case 0x59: case 0x93:
return "cooperative command-queue waits are not implemented";
case 0x84: case 0x85: case 0x86: case 0x88:
return "separate CE/DE execution and counter synchronization are not implemented";
case 0x43: case 0x46: case 0x47: case 0x48: case 0x49: case 0x58:
return "guest cache actions, GPU events and interrupt delivery are not implemented";
case 0x8e: return "GPU LOD statistics are not implemented; synthetic results are forbidden";
case 0x28: case 0x41: case 0x68: case 0x78:
return "opcode is named but has no handler in the reference dispatch table";
default: return "opcode is not known in the reference";
}
}
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require(packet.size() >= 2, "truncated PM4 header or payload");
const auto header = packet[0];
require((header & 0xc0000000u) == 0xc0000000u, "unsupported PM4 packet type");
require(packet.size() == ((header >> 16u) & 0x3fffu) + 2u, "invalid PM4 packet size");
const auto opcode = (header >> 8u) & 0xffu;
const auto size = [&](std::size_t count) { require(packet.size() == count, "invalid packet size"); };
const auto graphics = [&] { require(queue == 0, "graphics packet in compute queue"); };
const auto reason = UnsupportedReason(header);
if (!reason.empty()) throw std::runtime_error(std::string(reason));
if (opcode == 0x10) {
require((header & 3u) == 0, "unsupported NOP header flags");
switch ((header >> 2u) & 0x3fu) {
case 0:
require((packet[1] & 0xffff0000u) != 0x68750000u, "typed user-data and legacy flip markers are not implemented");
break;
case 0x09: size(2); break;
case 0x0b: {
const auto data = std::as_bytes(packet.subspan(1));
require(std::find(data.begin(), data.end(), std::byte{}) != data.end(), "unterminated marker text");
break;
}
case 0x0c: break;
case 0x17: graphics(); size(6); break;
case 0x1a:
graphics();
require(packet.size() == 3 || packet.size() == 5, "invalid context-state packet size");
require(packet[1] <= 3, "unknown context-state operation");
require(std::all_of(packet.begin() + 2, packet.end(), [](auto value) { return value == 0; }), "context-state trailing fields are not implemented");
break;
}
return;
}
require((header & 0xffu) == 0 || (opcode == 0x11 && (header & 0xffu) == 2), "PM4 header flags are not implemented");
switch (opcode) {
case 0x11:
size(4);
require(packet[1] == 1 && (packet[2] & 7u) == 0 && packet[3] <= 0xffffu, "unsupported indirect base index, alignment or address bits");
if ((header & 2u) == 0) graphics();
break;
case 0x12: graphics(); size(2); require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits"); break;
case 0x13: case 0x2f: graphics(); size(2); break;
case 0x26: graphics(); size(3); break;
case 0x2a: graphics(); size(2); require(packet[1] <= 3, "unsupported index-type modifiers"); break;
case 0x15: size(5); require((packet[4] & ~0x8000u) == 0x41u, "dispatch modifiers are not implemented"); break;
case 0x16:
require(packet.size() == 3 || packet.size() == 4, "invalid indirect dispatch size");
require((packet.back() & ~0x8000u) == 0x41u, "indirect dispatch modifiers are not implemented");
break;
case 0x42: size(2); require(packet[1] == 0, "unsupported PFP_SYNC_ME payload"); break;
case 0x63: case 0x64: case 0x9f:
if (opcode != 0x63) graphics();
size(5);
require((packet[1] & 3u) == 0 && packet[3] == 0x80000000u && packet[4] <= 0x3fffu, "unsupported indirect-register address or control fields");
break;
case 0x69: case 0x76: case 0x79: case 0x7a: {
if (opcode != 0x76) graphics();
require(packet.size() >= 3, "register packet has no values");
if (opcode == 0x7a) require((packet[1] & 0xf0000000u) == 0 || (packet.size() == 3 && packet[1] == 0x20000243u), "indexed register bank selection is not implemented");
const auto offset = registerOffset(packet[1]);
require(packet.size() - 2 <= 0x10000u - offset, "register range overflow");
break;
}
case 0x81:
graphics();
require(packet[1] <= 0xbffcu && (packet[1] & 3u) == 0 && packet.size() - 2 <= 0x3000u - packet[1] / 4u, "constant RAM write range overflow or misalignment");
break;
case 0x83:
graphics(); size(5);
require(packet[1] <= 0xbffcu && (packet[1] & 3u) == 0 && packet[2] <= 0x3000u - packet[1] / 4u, "constant RAM dump range overflow or misalignment");
break;
case 0x37: {
require(packet.size() >= 5, "WRITE_DATA has no data");
require((packet[1] & ~0x00110f00u) == 0, "WRITE_DATA engine, cache or reserved fields are not implemented");
const auto destination = (packet[1] >> 8u) & 0xfu;
require(destination == 1 || destination == 2 || (queue != 0 && destination == 5), "WRITE_DATA register or GDS destination is not implemented");
require((packet[2] & 3u) == 0, "misaligned WRITE_DATA destination");
break;
}
case 0x40: {
size(6);
require((packet[1] & ~0x40110f0fu) == 0, "COPY_DATA engine, cache or reserved fields are not implemented");
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
const auto destination = ((packet[1] >> 8u) & 0xfu) << 1u;
require(destination == 2 || destination == 4, "COPY_DATA register or GDS destination is not implemented");
require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11, "COPY_DATA register, GDS or reference-clock source is not implemented");
require(source < 10 || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented");
break;
}
case 0x50:
size(7);
require((packet[1] & ~0xe0300001u) == 0, "DMA_DATA cache or reserved fields are not implemented");
require(memorySelector(dmaDestination(packet)), "DMA_DATA register, GDS or prefetch destination is not implemented");
require(memorySelector(dmaSource(packet)) || dmaSource(packet) == 2, "DMA_DATA register or GDS source is not implemented");
require(dmaSource(packet) != 2 || packet[3] == 0, "DMA_DATA immediate exceeds 32 bits");
break;
default: throw std::runtime_error("known packet has no validator");
}
}
bool AccessesMemory(std::uint32_t header) {
switch ((header >> 8u) & 0xffu) {
case 0x16: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true;
default: return false;
}
}
std::array<std::uint32_t, 5> ResolveDispatch(std::span<const std::uint32_t> packet, const QueueState& queue) {
std::uint64_t source = 0;
if (packet.size() == 4) source = address(packet[1], packet[2]);
else {
require(queue.dispatchIndirectBase != 0, "indirect dispatch base has not been set");
require(packet[1] <= std::numeric_limits<std::uint64_t>::max() - queue.dispatchIndirectBase, "indirect dispatch address overflow");
source = queue.dispatchIndirectBase + packet[1];
}
std::array<std::uint32_t, 5> result{0xc0031500u, 0, 0, 0, packet.back()};
GuestMemory::Read(source, std::as_writable_bytes(std::span(result).subspan(1, 3)), 4);
return result;
}
void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
switch (opcode) {
case 0x10:
switch ((packet[0] >> 2u) & 0x3fu) {
case 0: return;
case 0x09: queue = QueueState{}; return;
case 0x0b: queue.markers.emplace_back(reinterpret_cast<const char*>(packet.data() + 1)); return;
case 0x0c:
require(!queue.markers.empty(), "marker stack underflow");
queue.markers.pop_back();
return;
case 0x1a:
switch (packet[1]) {
case 0: queue.ClearContext(); break;
case 1: case 3:
require(!queue.savedContext.has_value(), "context state is already pushed");
queue.savedContext = queue.context;
if (packet[1] == 3) queue.ClearContext();
break;
case 2:
require(queue.savedContext.has_value(), "context state has not been pushed");
queue.context = std::move(*queue.savedContext);
queue.savedContext.reset();
break;
}
return;
default: throw std::runtime_error("custom packet requires driver execution");
}
case 0x11:
((packet[0] & 2u) == 0 ? queue.drawIndirectBase : queue.dispatchIndirectBase) = address(packet[2], packet[3]);
return;
case 0x12: queue.ClearContext(); return;
case 0x13: queue.indexBufferSize = packet[1]; return;
case 0x26: queue.indexBase = address(packet[1], packet[2]); return;
case 0x2a: queue.indexType = packet[1]; return;
case 0x2f: queue.instanceCount = packet[1]; return;
case 0x63: case 0x64: case 0x9f: {
std::vector<std::uint32_t> pairs(static_cast<std::size_t>(packet[4]) * 2);
GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4);
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
return;
}
case 0x69: case 0x76: case 0x79: case 0x7a: {
const auto offset = registerOffset(packet[1]);
for (std::size_t i = 2; i < packet.size(); ++i) writeRegister(queue, opcode, offset + static_cast<std::uint32_t>(i - 2), packet[i]);
return;
}
case 0x81:
std::copy(packet.begin() + 2, packet.end(), queue.constantRam.begin() + packet[1] / 4);
return;
case 0x83:
GuestMemory::Write(address(packet[3], packet[4]), std::as_bytes(std::span(queue.constantRam).subspan(packet[1] / 4, packet[2])), 4);
return;
case 0x37: {
const auto destination = address(packet[2], packet[3]);
if ((packet[1] & 0x10000u) != 0) {
for (const auto& value : packet.subspan(4)) GuestMemory::Write(destination, std::as_bytes(std::span(&value, 1)), 4);
} else GuestMemory::Write(destination, std::as_bytes(packet.subspan(4)), 4);
return;
}
case 0x40: {
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
copyMemory(address(packet[2], packet[3]), address(packet[4], packet[5]), (packet[1] & 0x10000u) != 0 ? 8 : 4, source >= 10);
return;
}
case 0x50:
copyMemory(address(packet[2], packet[3]), address(packet[4], packet[5]), packet[6] & 0x3ffffffu, dmaSource(packet) == 2);
return;
default: throw std::runtime_error("packet requires driver execution");
}
}
}

View File

@ -0,0 +1,225 @@
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <cstdio>
#include <set>
#include <stdexcept>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
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::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Execute(packet, state);
}
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, "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() == 54, "reference opcode catalog is incomplete");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known");
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) check(AgcDriver::Pm4::Name(makePacket(0x10, {0}, id << 2)[0]) == name, "custom opcode name mismatch");
}
void testRegisters() {
AgcDriver::QueueState state;
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");
packet[1] = 0x1000;
packet[2] = 0;
expectFailure([&] { execute(state, packet); }, "guest");
packet[3] = 0;
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");
expectFailure([&] { execute(state, makePacket(0x7a, {0x10000010, 1})); }, "bank selection");
expectFailure([&] { execute(state, makePacket(0x69, {0xffff, 1, 2})); }, "overflow");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x9f, {0, 0, 0x80000000, 0}), 0x20); }, "compute");
}
void testContextAndBases() {
AgcDriver::QueueState state;
execute(state, makePacket(0x69, {0x10, 17}));
execute(state, makePacket(0x76, {0x20c, 2}));
execute(state, makePacket(0x10, {3, 0}, 0x68));
check(state.context.empty() && state.shader.at(0x20c) == 2, "push-clear reset wrong state");
expectFailure([&] { execute(state, makePacket(0x10, {1, 0}, 0x68)); }, "already pushed");
execute(state, makePacket(0x69, {0x10, 19}));
execute(state, makePacket(0x10, {2, 0}, 0x68));
check(state.context.at(0x10) == 17, "pop did not restore context");
expectFailure([&] { execute(state, makePacket(0x10, {2, 0}, 0x68)); }, "not been pushed");
alignas(8) std::array<std::uint32_t, 4> arguments{7, 8, 9, 0};
execute(state, makePacket(0x11, {1, low(arguments.data()), high(arguments.data())}, 2));
auto packet = makePacket(0x16, {0, 0x8041});
AgcDriver::Pm4::Validate(packet, 0);
auto resolved = AgcDriver::Pm4::ResolveDispatch(packet, state);
check(resolved == std::array<std::uint32_t, 5>{0xc0031500, 7, 8, 9, 0x8041}, "base-relative dispatch arguments changed");
packet = makePacket(0x16, {low(arguments.data()), high(arguments.data()), 0x41});
AgcDriver::Pm4::Validate(packet, 0x20);
check(AgcDriver::Pm4::ResolveDispatch(packet, state)[3] == 9, "absolute indirect dispatch arguments changed");
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));
expectFailure([&] { execute(state, makePacket(0x10, {0}, 0x30)); }, "underflow");
execute(state, makePacket(0x10, {0}, 0x24));
check(state.shader.empty() && state.context.empty() && state.dispatchIndirectBase == 0 && state.indexBase == 0 && !state.savedContext, "dispatch reset retained state");
}
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(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");
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(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");
expectFailure([&] { execute(state, makePacket(0x50, {0x60100000, low(source.data()), high(source.data()), 0, 0, 4})); }, "GDS");
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 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(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");
destination[0] = 0;
const auto draw = makePacket(0x2d, {3, 2});
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");
}
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");
}
}
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;
}
testCatalog();
testRegisters();
testContextAndBases();
testMemory();
testCopies();
testDriverSubmission();
LibcRunShutdown_nid_postfix();
std::puts("PM4 catalog, registers, state, memory 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;
}
}

View File

@ -6,7 +6,7 @@
#include <stdexcept>
#include <string>
int main() {
int main(int argc, char** argv) {
try {
alignas(256) const std::array<std::uint32_t, 1> code{0xbf810000};
Shader shader{};
@ -23,6 +23,15 @@ int main() {
0xc0031500, 1, 1, 1, 0x8041
};
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
const std::array<std::uint32_t, 3> arguments{1, 1, 1};
if (argc == 2 && std::string(argv[1]) == "indirect") {
const auto argumentAddress = reinterpret_cast<std::uintptr_t>(arguments.data());
commands[7] = 0xc0021600;
commands[8] = static_cast<std::uint32_t>(argumentAddress);
commands[9] = static_cast<std::uint32_t>(argumentAddress >> 32u);
commands[10] = 0x8041;
packet.dw_num = 11;
}
sceAgcDriverSubmitAcb(0x20, &packet);
try {
AgcDriverWaitIdle_nid_postfix();