From 9837c69f0d5a725a9768bb66ec1fac56eee76452 Mon Sep 17 00:00:00 2001 From: DotDebian Date: Mon, 5 Oct 2026 10:05:28 +0200 Subject: [PATCH] feat(agc): implement sceAgcDcbPrimeUtcl2 and its size query The DCB variant threw "not implemented" while the ACB one wrote a 5-dword packet. Both now go through Agc::Command::WritePrimeUtcl2 and the size query returns 20 bytes. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Acb/src/Memory.cpp | 9 +- core/libs/prx/libSceAgc/CMakeLists.txt | 20 ++++ .../prx/libSceAgc/Command/include/Memory.hpp | 1 + .../libs/prx/libSceAgc/Command/src/Memory.cpp | 5 + .../prx/libSceAgc/DcbState/src/Display.cpp | 10 +- core/libs/prx/libSceAgc/tests/PrimeUtcl2.cpp | 99 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/PrimeUtcl2.cpp diff --git a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp index c2b181de8..1a93bdbc2 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp @@ -47,14 +47,7 @@ std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize() { } std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, std::uint32_t sizeInBytes) { - const auto guestAddress = reinterpret_cast(address); - auto* packet = Agc::Command::Allocate(buf, 5, __func__); - packet[0] = Agc::Command::Header(0x10u, 5); - packet[1] = 0; - packet[2] = static_cast(guestAddress); - packet[3] = static_cast(guestAddress >> 32u); - packet[4] = sizeInBytes; - return packet; + return Agc::Command::WritePrimeUtcl2(buf, address, sizeInBytes, __func__); } std::uint32_t APS5_VABI sceAgcAcbPrimeUtcl2GetSize() { diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index e3cf070b9..4e9a781f8 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -151,3 +151,23 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_prime_utcl2_tests EXCLUDE_FROM_ALL + tests/PrimeUtcl2.cpp + Command/src/Packet.cpp + Command/src/Memory.cpp + Acb/src/Memory.cpp + DcbFlow/src/Memory.cpp + DcbState/src/Display.cpp +) +target_include_directories(agc_prime_utcl2_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_prime_utcl2_tests PRIVATE libc) +configure_windows_unwind(agc_prime_utcl2_tests) +if(BUILD_TESTING) + set_target_properties(agc_prime_utcl2_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_prime_utcl2_tests COMMAND agc_prime_utcl2_tests) + if(WIN32) + set_tests_properties(agc_prime_utcl2_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgc/Command/include/Memory.hpp b/core/libs/prx/libSceAgc/Command/include/Memory.hpp index 9c19b6599..9c5fe2edf 100644 --- a/core/libs/prx/libSceAgc/Command/include/Memory.hpp +++ b/core/libs/prx/libSceAgc/Command/include/Memory.hpp @@ -8,6 +8,7 @@ namespace Agc::Command { std::uint32_t* WriteDma(CommandBuffer* buffer, bool compute, std::uint8_t engine, std::uint8_t dst, std::uint8_t dstCachePolicy, std::uint64_t dstAddress, std::uint8_t src, std::uint8_t srcCachePolicy, std::uint64_t srcAddress, std::uint32_t numBytes, std::uint8_t waitForPrevious, std::uint8_t writeConfirm, std::uint8_t blockEngine, const char* function); std::uint32_t* WriteCopyData(CommandBuffer* buffer, bool compute, std::uint8_t dst, std::uint8_t dstCachePolicy, std::uint64_t dstAddress, std::uint8_t src, std::uint8_t srcCachePolicy, std::uint64_t srcAddressOrImmediate, std::uint8_t itemSize, std::uint8_t writeConfirm, const char* function); std::uint32_t* WriteAtomicMem(CommandBuffer* buffer, std::uint8_t atomicOp, std::uint8_t command, std::uint8_t cachePolicy, const volatile void* address, std::uint64_t srcData, std::uint64_t compareData, std::uint16_t loopInterval, const char* function); +std::uint32_t* WritePrimeUtcl2(CommandBuffer* buffer, const volatile void* address, std::uint32_t sizeInBytes, const char* function); std::uint32_t* WriteData(CommandBuffer* buffer, bool compute, std::uint8_t dst, std::uint8_t cachePolicy, std::uint64_t address, const void* data, std::uint32_t count, std::uint8_t increment, std::uint8_t writeConfirm, const char* function); std::uint32_t* WriteWait(CommandBuffer* buffer, std::uint8_t size, std::uint8_t compareFunction, std::uint8_t operation, std::uint8_t cachePolicy, const volatile void* address, std::uint64_t reference, std::uint64_t mask, std::uint32_t pollCycles, const char* function); std::uint32_t* ValidateWait(std::uint32_t* packet, const char* function); diff --git a/core/libs/prx/libSceAgc/Command/src/Memory.cpp b/core/libs/prx/libSceAgc/Command/src/Memory.cpp index 86d799379..15d897300 100644 --- a/core/libs/prx/libSceAgc/Command/src/Memory.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Memory.cpp @@ -49,6 +49,11 @@ std::uint32_t* WriteAtomicMem(CommandBuffer* buffer, std::uint8_t atomicOp, std: return Emit(buffer, 0x1eu, {atomicOp | (static_cast(command) << 8u) | (static_cast(cachePolicy) << 25u), static_cast(guestAddress), static_cast(guestAddress >> 32u), static_cast(srcData), static_cast(srcData >> 32u), static_cast(compareData), static_cast(compareData >> 32u), loopInterval}, function); } +std::uint32_t* WritePrimeUtcl2(CommandBuffer* buffer, const volatile void* address, std::uint32_t sizeInBytes, const char* function) { + const auto guestAddress = reinterpret_cast(address); + return Emit(buffer, 0x10u, {0, static_cast(guestAddress), static_cast(guestAddress >> 32u), sizeInBytes}, function); +} + std::uint32_t* WriteData(CommandBuffer* buffer, bool compute, std::uint8_t dst, std::uint8_t cachePolicy, std::uint64_t address, const void* data, std::uint32_t count, std::uint8_t increment, std::uint8_t writeConfirm, const char* function) { Require(data != nullptr && count != 0 && count <= 0x3ffdu, function, "invalid write data payload"); CheckBits(dst, compute ? 0xfu : 0x1fu, function); diff --git a/core/libs/prx/libSceAgc/DcbState/src/Display.cpp b/core/libs/prx/libSceAgc/DcbState/src/Display.cpp index 9b379b97d..f9594cfe8 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Display.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Display.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgc/DcbState/include/Display.hpp" +#include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include @@ -21,16 +22,11 @@ uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_hand } uint32_t* APS5_VABI sceAgcDcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, uint32_t size_in_bytes) { - (void)buf; - (void)address; - (void)size_in_bytes; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return Agc::Command::WritePrimeUtcl2(buf, address, size_in_bytes, __func__); } std::uint32_t APS5_VABI sceAgcDcbPrimeUtcl2GetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 20; } } diff --git a/core/libs/prx/libSceAgc/tests/PrimeUtcl2.cpp b/core/libs/prx/libSceAgc/tests/PrimeUtcl2.cpp new file mode 100644 index 000000000..b1e702d24 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/PrimeUtcl2.cpp @@ -0,0 +1,99 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer*, const volatile void*, std::uint32_t); +std::uint32_t APS5_VABI sceAgcAcbPrimeUtcl2GetSize(); +std::uint32_t* APS5_VABI sceAgcDcbPrimeUtcl2(CommandBuffer*, const volatile void*, std::uint32_t); +std::uint32_t APS5_VABI sceAgcDcbPrimeUtcl2GetSize(); +} + +namespace { + +constexpr std::uint32_t Sentinel = 0xabcdef01u; +constexpr std::uint64_t Address = 0x0000112233440000ull; +constexpr std::uint32_t SizeInBytes = 0x00345000u; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action) { + try { + action(); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error("expected invalid input to fail"); +} + +struct Storage { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + + Storage() { + words.fill(Sentinel); + } + + void limitTo(std::uint32_t count) { + buffer.cursor_down = buffer.cursor_up + count; + } +}; + +std::uint32_t* writeCompute(CommandBuffer* buffer) { + return sceAgcAcbPrimeUtcl2(buffer, reinterpret_cast(Address), SizeInBytes); +} + +std::uint32_t* writeDraw(CommandBuffer* buffer) { + return sceAgcDcbPrimeUtcl2(buffer, reinterpret_cast(Address), SizeInBytes); +} + +template +void testCommand(TWriter writer, TSize size) { + const std::array expected{0xc0031000u, 0u, 0x33440000u, 0x1122u, SizeInBytes}; + const auto count = static_cast(expected.size()); + check(size() == count * sizeof(std::uint32_t), "size query does not match the prime packet"); + + Storage storage; + auto* packet = writer(&storage.buffer); + check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "incorrect prime packet"); + check(storage.buffer.cursor_up == packet + size() / sizeof(std::uint32_t), "cursor advance does not match the size query"); + check(std::all_of(storage.words.begin() + count, storage.words.end(), [](std::uint32_t word) { return word == Sentinel; }), "prime command overwrote following words"); + + auto* second = writer(&storage.buffer); + check(second == packet + count && std::equal(expected.begin(), expected.end(), second), "second prime packet does not follow the first"); + + Storage exact; + exact.limitTo(size() / sizeof(std::uint32_t)); + check(writer(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.buffer.cursor_down, "prime packet does not fill the queried size"); + + Storage shortBuffer; + shortBuffer.limitTo(size() / sizeof(std::uint32_t) - 1u); + const auto before = shortBuffer.words; + expectFailure([&] { writer(&shortBuffer.buffer); }); + check(shortBuffer.words == before && shortBuffer.buffer.cursor_up == shortBuffer.words.data(), "failed prime write modified the buffer"); + + expectFailure([&] { writer(nullptr); }); +} + +} + +int main() { + try { + testCommand(writeCompute, sceAgcAcbPrimeUtcl2GetSize); + testCommand(writeDraw, sceAgcDcbPrimeUtcl2GetSize); + check(sceAgcAcbPrimeUtcl2GetSize() == sceAgcDcbPrimeUtcl2GetSize(), "compute and draw prime sizes differ"); + std::puts("AGC prime UTCL2 tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c7520ce93..ddd0b29ef 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -37,6 +37,7 @@ Throughout the project, every function at every stage either **does exactly what - [dolOmWH+huQ](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name - [V++UgBtQhn0](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name - [gQkqkLttcpw](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature +- [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted