From 8289f30a0398ba8fb3b583016d1b7f250f08b903 Mon Sep 17 00:00:00 2001 From: Chad Date: Sun, 4 Oct 2026 18:28:50 -0500 Subject: [PATCH 01/43] feat(libs): implement the AMPR nops and the 04_00 completion writes sceAmprCommandBufferNop/NopWithData and their measure functions record a Nop of the requested dwords. The 04_00 forms of WaitOnAddress and of the time counter, counter, counter pair and event queue writes record the same commands as the current exports; their at-start flag has no effect, as commands run in order. Argument checks follow ampr_emu. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAmpr/Export.cpp | 105 +++++++++++++++++----------- core/libs/tests/GuestAmpr.cpp | 76 ++++++++++++++++++++ 2 files changed, 139 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 51d8032fd..9817879ea 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include +#include static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; @@ -35,6 +36,26 @@ static bool ValidWait(std::uint32_t compare, std::uint32_t flush) { return compare < 4u && flush < 2u; } +static constexpr std::uint64_t MeasureInvalid = static_cast(SCE_KERNEL_ERROR_EINVAL); + +static std::uint64_t NopBytes(std::uint32_t dwords) { + return sizeof(Apr::CommandHeader) + std::uint64_t{dwords} * 4u; +} + +static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t bytes = NopBytes(dwords); + if (!buffer->base || bytes > buffer->size - buffer->offset) return SCE_AMPR_ERROR_BUFFER_FULL; + const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; + std::uint8_t* destination = buffer->base + buffer->offset; + std::memset(destination, 0, bytes); + std::memcpy(destination, &header, sizeof(header)); + if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u); + buffer->offset += static_cast(bytes); + ++buffer->numCommands; + return 0; +} + static std::uint64_t MarkerBytes(const char* text) { return sizeof(Apr::MarkerCommand) + ((std::strlen(text) + 8) & ~std::uint64_t{7}); } @@ -215,14 +236,15 @@ uint32_t APS5_VABI sceAmprCommandBufferGetType(const Apr::CommandBufferObject* b return static_cast(buffer->type); } -int APS5_VABI sceAmprCommandBufferNop() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords) { + if (dwords == 0u || dwords > 16u) return SCE_KERNEL_ERROR_EINVAL; + return AppendNop(buffer, dwords, nullptr); } -int APS5_VABI sceAmprCommandBufferNopWithData() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { + if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; + if (dwords != 0u && !data) throw std::invalid_argument("sceAmprCommandBufferNopWithData: null data"); + return AppendNop(buffer, dwords, data); } int APS5_VABI sceAmprCommandBufferPopMarker(Apr::CommandBufferObject* buffer) { @@ -261,9 +283,9 @@ int APS5_VABI sceAmprCommandBufferSetMarkerWithColor(Apr::CommandBufferObject* b return AppendMarker(buffer, Apr::Opcode::SetMarker, text); } -int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t reference, std::uint8_t compare, std::uint8_t flush) { + if (!ValidWriteAddress(address) || compare > 6u || flush > 1u) return SCE_KERNEL_ERROR_EINVAL; + return AppendCommand(buffer, Apr::Opcode::WaitOnAddress, Apr::WaitCommand{{}, reinterpret_cast(address), reference, ~0ull, 0, compare}); } int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00() { @@ -271,19 +293,19 @@ int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00() { return 0; } -int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint8_t counter, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteAddressFromCounterPairOnCompletion(buffer, address, counter); } -int APS5_VABI sceAmprCommandBufferWriteAddressFromCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounter_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint8_t counter, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteAddressFromCounterOnCompletion(buffer, address, counter); } -int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteAddressFromTimeCounterOnCompletion(buffer, address); } int APS5_VABI sceAmprCommandBufferWriteAddress_04_00(Apr::CommandBufferObject* buffer, uint64_t* address, uint64_t value, uint32_t flags) { @@ -300,9 +322,9 @@ int APS5_VABI sceAmprCommandBufferWriteCounter_04_00() { return 0; } -int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00(Apr::CommandBufferObject* buffer, std::uint64_t equeue, std::int32_t ident, std::uint64_t data, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteKernelEventQueueOnCompletion(buffer, equeue, ident, data); } int APS5_VABI sceAmprMeasureCommandSizeMapBegin() { @@ -320,14 +342,14 @@ int APS5_VABI sceAmprMeasureCommandSizeMapEnd() { return 0; } -int APS5_VABI sceAmprMeasureCommandSizeNop() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNop(std::uint32_t dwords) { + if (dwords == 0u || dwords > 16u) return MeasureInvalid; + return NopBytes(dwords); } -int APS5_VABI sceAmprMeasureCommandSizeNopWithData() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNopWithData(std::uint32_t dwords) { + if (dwords == 0u || dwords > 16u) return MeasureInvalid; + return NopBytes(dwords - 1u); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker() { @@ -376,9 +398,9 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char* return MeasureMarker(text); } -int APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00(volatile std::uint64_t* address, std::uint64_t, std::uint8_t compare, std::uint8_t flush) { + if (!ValidWriteAddress(address) || compare > 6u || flush > 1u) return MeasureInvalid; + return sizeof(Apr::WaitCommand); } int APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00() { @@ -386,19 +408,19 @@ int APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00() { return 0; } -int APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t* address, std::uint8_t counter) { + if (!ValidWriteAddress(address) || (counter & 0x81u) != 0u) return MeasureInvalid; + return sizeof(Apr::WriteAddressFromCounterCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(volatile std::uint64_t* address, std::uint8_t counter) { + if (!ValidWriteAddress(address) || !ValidCounter(counter)) return MeasureInvalid; + return sizeof(Apr::WriteAddressFromCounterCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(volatile std::uint64_t* address) { + if (!ValidWriteAddress(address)) return MeasureInvalid; + return sizeof(Apr::WriteAddressFromCounterCommand); } uint32_t APS5_VABI sceAmprMeasureCommandSizeWriteAddress_04_00(void) { @@ -410,9 +432,8 @@ int APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00() { return 0; } -int APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00(std::uint64_t, std::int32_t, std::uint64_t) { + return sizeof(Apr::WriteKernelEventQueueCommand); } } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 74dad2ece..b194afe06 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -33,6 +33,19 @@ int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounterOnCompletion(Apr::C int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterOnCompletion(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPairOnCompletion(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteKernelEventQueueOnCompletion(Apr::CommandBufferObject*, std::uint64_t, std::int32_t, std::uint64_t); +int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject*, std::uint32_t); +int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject*, std::uint32_t, const std::uint32_t*); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNop(std::uint32_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNopWithData(std::uint32_t); +int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00(volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); +int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t); +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounter_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t, std::uint64_t); +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t, std::uint64_t); +int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00(Apr::CommandBufferObject*, std::uint64_t, std::int32_t, std::uint64_t, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(volatile std::uint64_t*); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(volatile std::uint64_t*, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t*, std::uint8_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -212,6 +225,67 @@ void TestRejectedWaitsAndCounters() { Require(recorder.Offset() == 0 && recorder.Commands() == 0); } +void TestNops() { + Recorder recorder; + const std::uint32_t data[3] = {0x11111111u, 0x22222222u, 0x33333333u}; + for (std::uint32_t dwords = 1; dwords <= 16; ++dwords) { + const auto offset = recorder.Offset(); + const auto commands = recorder.Commands(); + Require(sceAmprCommandBufferNop(&recorder.buffer, dwords) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNop(dwords)); + } + const auto offset = recorder.Offset(); + const auto commands = recorder.Commands(); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, data) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); + Require(std::memcmp(recorder.memory.data() + offset + sizeof(Apr::CommandHeader), data, sizeof(data)) == 0); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + + const auto rejected = static_cast(static_cast(invalidArgument)); + Require(sceAmprCommandBufferNop(&recorder.buffer, 0) == invalidArgument); + Require(sceAmprCommandBufferNop(&recorder.buffer, 17) == invalidArgument); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 16, data) == invalidArgument); + Require(sceAmprMeasureCommandSizeNop(0) == rejected && sceAmprMeasureCommandSizeNop(17) == rejected); + Require(sceAmprMeasureCommandSizeNopWithData(0) == rejected && sceAmprMeasureCommandSizeNopWithData(17) == rejected); +} + +void TestVersionedCommands() { + Recorder recorder; + alignas(8) std::uint64_t value = 0x8000000000000005ull; + alignas(8) std::uint64_t single = 0; + alignas(8) std::uint64_t pair = 0; + alignas(8) std::uint64_t time = 0; + Require(sceAmprCommandBufferWaitOnAddress_04_00(&recorder.buffer, &value, 0x8000000000000003ull, 4, 0) == 0); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&recorder.buffer, &value, 1, 6, 1) == 0); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&recorder.buffer, &value, 0x8000000000000000ull, 5, 0) == 0); + Require(sceAmprCommandBufferWriteCounterOnCompletion(&recorder.buffer, 10, 3) == 0); + Require(sceAmprCommandBufferWriteCounterOnCompletion(&recorder.buffer, 11, 4) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounter_04_00(&recorder.buffer, &single, 10, 1) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterPair_04_00(&recorder.buffer, &pair, 10, 0) == 0); + Require(sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(&recorder.buffer, &time, 1) == 0); + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0); }); + Require(submitted.wait_for(std::chrono::seconds(10)) == std::future_status::ready); + Require(submitted.get() == 0 && single == 3 && pair == (3ull | (4ull << 32u)) && time != 0); + + const auto rejected = static_cast(static_cast(invalidArgument)); + Recorder empty; + Require(sceAmprCommandBufferWaitOnAddress_04_00(&empty.buffer, nullptr, 0, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&empty.buffer, &value, 0, 7, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&empty.buffer, &value, 0, 0, 2) == invalidArgument); + Require(sceAmprCommandBufferWriteAddressFromCounterPair_04_00(&empty.buffer, &pair, 11, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(&empty.buffer, nullptr, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteKernelEventQueue_04_00(&empty.buffer, 0, 1, 0, 0) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); + Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(nullptr, 0, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(&value, 0, 7, 0) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(nullptr) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(&single, 128) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 11) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(&value, 0, 6, 1) == sizeof(Apr::WaitCommand)); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 10) == sizeof(Apr::WriteAddressFromCounterCommand)); +} + } int main() { @@ -226,5 +300,7 @@ int main() { TestWaits(); TestCounters(); TestRejectedWaitsAndCounters(); + TestNops(); + TestVersionedCommands(); return 0; } From 6f5cb736ae83e834dc014d212ffff33de481f627 Mon Sep 17 00:00:00 2001 From: Chad Date: Sun, 4 Oct 2026 18:48:45 -0500 Subject: [PATCH 02/43] fix(libs): match the AMPR 04_00 measure checks of ampr_emu The WriteAddressFromCounter_04_00 measure checks only the address and the counter pair measure only the address and an even start, as in ampr_emu. TechnicalDebt records that AMPR commands are larger than the console records, which affects titles that pad with Nop to an exact offset, and that the at-start flag of the 04_00 writes is ignored. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAmpr/Export.cpp | 5 +++-- core/libs/tests/GuestAmpr.cpp | 4 +++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 9817879ea..df54ef647 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -409,12 +409,13 @@ int APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00() { } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t* address, std::uint8_t counter) { - if (!ValidWriteAddress(address) || (counter & 0x81u) != 0u) return MeasureInvalid; + if (!ValidWriteAddress(address) || (counter & 1u) != 0u) return MeasureInvalid; return sizeof(Apr::WriteAddressFromCounterCommand); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(volatile std::uint64_t* address, std::uint8_t counter) { - if (!ValidWriteAddress(address) || !ValidCounter(counter)) return MeasureInvalid; + (void)counter; + if (!ValidWriteAddress(address)) return MeasureInvalid; return sizeof(Apr::WriteAddressFromCounterCommand); } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index b194afe06..80b105261 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -280,7 +280,9 @@ void TestVersionedCommands() { Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(nullptr, 0, 0, 0) == rejected); Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(&value, 0, 7, 0) == rejected); Require(sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(nullptr) == rejected); - Require(sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(&single, 128) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(&single, 128) == sizeof(Apr::WriteAddressFromCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(nullptr, 0) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 128) == sizeof(Apr::WriteAddressFromCounterCommand)); Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 11) == rejected); Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(&value, 0, 6, 1) == sizeof(Apr::WaitCommand)); Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 10) == sizeof(Apr::WriteAddressFromCounterCommand)); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ff3427580..9fb733f1f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -25,6 +25,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order +- [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature From a347b02b48a5d5d59b5e7f45f7538b4868255a60 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:11:15 +0300 Subject: [PATCH 03/43] feat(pad): map Xbox View button to PS5 touchpad Controllers other than DualShock 4 and DualSense lack a dedicated touchpad button. Maps the SDL Back button (View on Xbox) to PadButton::TouchPad for non-PlayStation controllers. Co-Authored-By: saulo benigno --- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 3469cb70d..5f1148d27 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -132,6 +132,8 @@ PadInputState PadInput::sampleController() const { addButton(SDL_CONTROLLER_BUTTON_Y, Pad::PadButton::Triangle); addButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER, Pad::PadButton::L1); addButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, Pad::PadButton::R1); + const auto type = SDL_GameControllerGetType(controller); + if (type != SDL_CONTROLLER_TYPE_PS4 && type != SDL_CONTROLLER_TYPE_PS5) addButton(SDL_CONTROLLER_BUTTON_BACK, Pad::PadButton::TouchPad); addButton(SDL_CONTROLLER_BUTTON_START, Pad::PadButton::Options); addButton(SDL_CONTROLLER_BUTTON_LEFTSTICK, Pad::PadButton::L3); addButton(SDL_CONTROLLER_BUTTON_RIGHTSTICK, Pad::PadButton::R3); From aad1be52e8a36f480e4e98fef3579f7b9975a62c Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:13:26 +0300 Subject: [PATCH 04/43] fix(pad): synthesize touch contact for Xbox View --- core/libs/prx/libScePad/CMakeLists.txt | 2 +- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 7 ++++++- core/libs/tests/GuestPad.cpp | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/CMakeLists.txt b/core/libs/prx/libScePad/CMakeLists.txt index ccb006f0d..75308dd0f 100644 --- a/core/libs/prx/libScePad/CMakeLists.txt +++ b/core/libs/prx/libScePad/CMakeLists.txt @@ -14,7 +14,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_pad_tests ${LIBS_INCLUDE_DIR}/tests/GuestPad.cpp) - target_include_directories(guest_pad_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_include_directories(guest_pad_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) target_link_libraries(guest_pad_tests PRIVATE libScePad libc) configure_windows_unwind(guest_pad_tests) add_test(NAME guest_pad COMMAND guest_pad_tests) diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5f1148d27..cf4ec4491 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -133,7 +133,8 @@ PadInputState PadInput::sampleController() const { addButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER, Pad::PadButton::L1); addButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, Pad::PadButton::R1); const auto type = SDL_GameControllerGetType(controller); - if (type != SDL_CONTROLLER_TYPE_PS4 && type != SDL_CONTROLLER_TYPE_PS5) addButton(SDL_CONTROLLER_BUTTON_BACK, Pad::PadButton::TouchPad); + const bool viewPressed = type != SDL_CONTROLLER_TYPE_PS4 && type != SDL_CONTROLLER_TYPE_PS5 && readButton(SDL_CONTROLLER_BUTTON_BACK); + if (viewPressed) result.buttons |= static_cast(Pad::PadButton::TouchPad); addButton(SDL_CONTROLLER_BUTTON_START, Pad::PadButton::Options); addButton(SDL_CONTROLLER_BUTTON_LEFTSTICK, Pad::PadButton::L3); addButton(SDL_CONTROLLER_BUTTON_RIGHTSTICK, Pad::PadButton::R3); @@ -187,6 +188,10 @@ PadInputState PadInput::sampleController() const { result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); } } + // View has no touch position, so use a neutral center contact when SDL reports none. + if (viewPressed && !result.touch[0].active && !result.touch[1].active) { + result.touch[0] = {true, 960, 471, 0}; + } return result; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 8abd4f04f..522d99171 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -1,4 +1,5 @@ #include "prx/libScePad/include/Pad.hpp" +#include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -42,6 +43,20 @@ static void CheckTiltCorrection(int handle) { PadPublishInput_nid_postfix(PadInputState{}); } +static void CheckTouchContact() { + PadInputState touch; + touch.buttons = static_cast(Pad::PadButton::TouchPad); + touch.touch[0] = {true, 960, 471, 0}; + PadPublishInput_nid_postfix(touch); + const auto data = Pad::ReadState(); + Require((data.buttons & static_cast(Pad::PadButton::TouchPad)) != 0); + Require(data.touch_data_touch_num == 1); + Require(data.touch_data_touch0_x == 960); + Require(data.touch_data_touch0_y == 471); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + int main() { constexpr int noHandle = static_cast(0x80920008); constexpr int user = 0x10000000; @@ -55,6 +70,7 @@ int main() { Require(scePadGetHandle(user, 0, 0) == handle); Require(scePadGetHandle(user, 2, 0) == handle); CheckTiltCorrection(handle); + CheckTouchContact(); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); From b6c7f1c7e30f204dea4d5b0ce7710367e4a4cbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 15:20:01 +0200 Subject: [PATCH 05/43] fix(shader): match v_dot2_f32_f16 and v_dot2c_f32_f16 to hardware Single rounding of the whole sum, f32 accumulator denormals flushed, NaN picked on the bits, clamp ignored and the v_dot2c accumulator read from the lane itself under DPP, all as measured on hardware. Co-Authored-By: Claude --- .../tests/execution/Float16Dot.cpp | 161 ++++++++++-------- .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 1 + .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterFloat64.cpp | 90 ++++++++++ .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../Translation/TranslationContext.hpp | 1 + .../Translation/src/FloatInstructions.cpp | 52 +++--- 8 files changed, 217 insertions(+), 91 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp index 7242e910c..f7d519ee1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -18,52 +17,94 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Inputs = 4; -constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Results = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array DotCode{ - 0x34020084, 0x34060085, 0xe0301000, 0x80000401, 0xe0301004, 0x80000501, 0xe0301008, 0x80000601, - 0xbf8c3f70, 0xcc13400a, 0x1c1a0b04, 0xcc13480b, 0x141a0b04, 0xcc13420c, 0xbc1a0b04, 0xcc13c00d, - 0x1c1a0b04, 0xe0701000, 0x80010a03, 0xe0701004, 0x80010b03, 0xe0701008, 0x80010c03, 0xe070100c, - 0x80010d03, 0xbf810000, +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0x7e140306, 0x04140b04, 0xcc13400b, + 0x1c1a0b04, 0xcc13420c, 0xbc1a0b04, 0xcc13410d, 0x5c1a0b04, 0xcc13480e, 0x141a0b04, 0xcc13c00f, + 0x1c1a0b04, 0x7e200306, 0x04200af2, 0x7e220306, 0x04220aff, 0x40003c00, 0x7e240306, 0x04240afa, + 0xff00b104, 0x7e260306, 0xcc135013, 0x0c1a0b04, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, + 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, }; -constexpr std::array, 8> Edges{{ - {0x3c003c00u, 0x3c003c00u, 0x00000000u}, {0x7c000000u, 0x3c000000u, 0x3f800000u}, {0x7c003c00u, 0x00003c00u, 0x3f800000u}, - {0xfc007c00u, 0x3c003c00u, 0x40000000u}, {0x80000000u, 0x00008000u, 0x80000000u}, {0x7bff7bffu, 0x7bff7bffu, 0xcf800000u}, - {0x38003800u, 0xb800b800u, 0x3e800000u}, {0x7e003c00u, 0x3c003c00u, 0x00000000u}, -}}; +constexpr std::uint32_t Rows[32][4] = { + {0x0001bc00u, 0x81bfacbau, 0xc541aa2fu, 0x00000000u}, + {0x01f61a79u, 0x3c000381u, 0xc1729fffu, 0x00000000u}, + {0x02c9be80u, 0xb252213du, 0x41880526u, 0x00000000u}, + {0x032f030cu, 0x0d88ca10u, 0x3f800000u, 0x00000000u}, + {0x04003848u, 0x2427b61cu, 0x3920d8c0u, 0x00000000u}, + {0x04008190u, 0x01c85a38u, 0xc1e5655cu, 0x00000000u}, + {0x00ba3faeu, 0x02bcc61fu, 0x439ff26au, 0x00000000u}, + {0x00bc0400u, 0xaed23800u, 0xaf78b10fu, 0x00000000u}, + {0x47a37c85u, 0x7ceb0000u, 0x80521c95u, 0x00000000u}, + {0x6bc64fd7u, 0x02f45f4du, 0xffc8ab86u, 0x00000000u}, + {0x034102d0u, 0xac427d00u, 0x7fa00000u, 0x00000000u}, + {0x00000000u, 0xf35dfc00u, 0x805c769au, 0x00000000u}, + {0x0000030cu, 0x8370c506u, 0x00000001u, 0x00000000u}, + {0x00002783u, 0x5f2c0896u, 0x7f800000u, 0x00000000u}, + {0x00010001u, 0xfc00004cu, 0xca63705au, 0x00000000u}, + {0x64ba817bu, 0x80000000u, 0x00000001u, 0x00000000u}, + {0x000083ffu, 0x7d00def6u, 0x95a99701u, 0x00000000u}, + {0x7c210c47u, 0x825c8000u, 0x45b66cc3u, 0x00000000u}, + {0x00003134u, 0x8000c266u, 0x8029d2f0u, 0x00000000u}, + {0x3b898000u, 0x0000e8b9u, 0x0034ccdcu, 0x00000000u}, + {0x00000000u, 0x04008000u, 0x5f834da7u, 0x00000000u}, + {0x00000001u, 0x366e51ddu, 0x7f7fffffu, 0x00000000u}, + {0x00000001u, 0xdc472c9du, 0x42750eeeu, 0x00000000u}, + {0x000000c8u, 0x83ff3c00u, 0x43380747u, 0x00000000u}, + {0x0000010cu, 0x7c00f56fu, 0x005df72bu, 0x00000000u}, + {0x00000377u, 0xdaabea88u, 0xf540cddbu, 0x00000000u}, + {0x000003cau, 0x5b9f7e00u, 0xcaba68d5u, 0x00000000u}, + {0x00000400u, 0x785b8ab8u, 0x7fa00000u, 0x00000000u}, + {0x0000051cu, 0x6f115c7fu, 0x0047beccu, 0x00000000u}, + {0x00000c55u, 0x3c00ec7fu, 0x263e4496u, 0x00000000u}, + {0x00000f28u, 0x9e2d0123u, 0x565f49ebu, 0x00000000u}, + {0x00001209u, 0x3fe3c10fu, 0x801b0c3eu, 0x00000000u} +}; -float Half(std::uint32_t bits) { - const std::uint32_t sign = (bits & 0x8000u) << 16u; - const std::uint32_t exponent = (bits >> 10u) & 0x1fu; - const std::uint32_t mantissa = bits & 0x3ffu; - if (exponent == 0x1fu) return std::bit_cast(sign | 0x7f800000u | (mantissa << 13u)); - if (exponent == 0u) return std::bit_cast(sign) + (sign != 0u ? -1.0f : 1.0f) * std::ldexp(static_cast(mantissa), -24); - return std::bit_cast(sign | ((exponent + 112u) << 23u) | (mantissa << 13u)); -} +constexpr std::uint32_t Expected[32][10] = { + {0xc541a901u, 0xc541a901u, 0x4541a901u, 0xc541ab5du, 0xc541aa2fu, 0xc541a901u, 0xc541ab5eu, 0xc541ab5eu, 0xc541aa30u, 0xc541aa2fu}, + {0xc1729fdfu, 0xc1729fdfu, 0x41729fdfu, 0xc172a01fu, 0xc172930du, 0xc1729fdfu, 0xc1729fc7u, 0xc1529fc7u, 0xc172a037u, 0xc172930du}, + {0x4187e315u, 0x4187e315u, 0xc187e315u, 0x41882737u, 0x418a9676u, 0x4187e315u, 0x41881a1au, 0x4184f11au, 0x41880521u, 0x418a9676u}, + {0x3f7fdb0fu, 0x3f7fdb0fu, 0xbf7fdb0fu, 0x3f801279u, 0x3f7fd966u, 0x3f7fdb0fu, 0xc1320000u, 0xc131fd3cu, 0x41a5a000u, 0x3f7fd966u}, + {0xbe511687u, 0xbe511687u, 0x3e511687u, 0x3e5166f4u, 0x3c105963u, 0xbe511687u, 0xbec36be5u, 0xbeb2cfe5u, 0x392b6e40u, 0x3c105963u}, + {0xc1e56f13u, 0xc1e56f13u, 0x41e56f13u, 0xc1e55ba5u, 0xc1e54c7cu, 0xc1e56f13u, 0x432a5354u, 0x432a5358u, 0x429ba4a9u, 0xc1e54c7cu}, + {0x439a1228u, 0x439a1228u, 0xc39a1228u, 0x43a5d2acu, 0x439ff26au, 0x439a1228u, 0x439ce2eau, 0x439ce2edu, 0x439ff25eu, 0x439ff26au}, + {0x37f5fb14u, 0x37f5fb14u, 0xb7f5fb14u, 0xb7f5fc0cu, 0xb5720f8bu, 0x37f5fb14u, 0x3f000000u, 0x3e92e000u, 0x3f75bfecu, 0xb5720f8bu}, + {0x7fd0a000u, 0x7fd0a000u, 0xffd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fdd6000u, 0x7fdd6000u, 0x7fdd6000u, 0x7fd0a000u}, + {0xffc8ab86u, 0xffc8ab86u, 0x7fc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0x7fd0a000u, 0xffc8ab86u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xffc00000u}, + {0xb974e480u, 0xb974e480u, 0x3974e480u, 0x3974e480u, 0xb1279400u, 0xb974e480u, 0xc0a0c000u, 0xc0a0c0dcu, 0xbe16f048u, 0xb1279400u}, + {0x7f800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, + {0xff800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0xff800000u, 0xffc00000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fc42000u, 0x7fe00000u}, + {0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0xffc42000u, 0x7fc42000u, 0x7fc42000u, 0x45b66cc3u, 0x45b66cc3u, 0x45b66cc3u, 0x7fc42000u}, + {0xbf052ae0u, 0xbf052ae0u, 0x3f052ae0u, 0x3f052ae0u, 0x80000000u, 0xbf052ae0u, 0xc04cc000u, 0xc04cc000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0xc50e5804u, 0x00000000u, 0xc5172000u, 0xc5172000u, 0xc3c494a0u, 0xc50e5804u}, + {0x5f834da7u, 0x5f834da7u, 0xdf834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u}, + {0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu}, + {0x42750eeeu, 0x42750eeeu, 0xc2750eeeu, 0x42750eeeu, 0x42750eeau, 0x42750eeeu, 0x427558beu, 0xc3f314e8u, 0x42750eeeu, 0x42750eeau}, + {0x43380748u, 0x43380748u, 0xc3380748u, 0x43380746u, 0x43380747u, 0x43380748u, 0x43390747u, 0x4339073fu, 0x43380747u, 0x43380747u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0x7f800000u, 0xffc00000u, 0xffc00000u, 0x7f800000u, 0xffc00000u, 0x7f800000u}, + {0xf540cddbu, 0xf540cddbu, 0x7540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0x3cb7c720u, 0x3cb7c720u, 0xbcb7c720u, 0xbcb7c720u, 0x3f106b70u, 0x3cb7c720u, 0x438fe000u, 0x46669f00u, 0x3d9bd158u, 0x3f106b70u}, + {0xbf9bd158u, 0xbf9bd158u, 0x3f9bd158u, 0x3f9bd158u, 0x398aa000u, 0xbf9bd158u, 0xc58fe000u, 0xc58fd000u, 0xbeb7c720u, 0x398aa000u}, + {0x565f49ebu, 0x565f49ebu, 0xd65f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu}, + {0xbaf43c38u, 0xbaf43c38u, 0x3af43c38u, 0x3af43c38u, 0x3abe63ecu, 0xbaf43c38u, 0xc021e000u, 0x3fb50000u, 0xba90cd60u, 0x3abe63ecu} +}; -std::uint32_t RandomHalf(std::uint64_t& state) { - state = state * 6364136223846793005ull + 1442695040888963407ull; - const auto bits = static_cast(state >> 33u); - return (bits & 0x8000u) | ((8u + (bits >> 16u) % 15u) << 10u) | (bits & 0x3ffu); -} +constexpr const char* Names[10] = {"v_dot2c", "v_dot2", "v_dot2 neg_lo src0/src2 neg_hi src1", "v_dot2 neg_lo src1 neg_hi src0", "v_dot2 op_sel src0", "v_dot2 clamp", "v_dot2c inline 1.0", "v_dot2c literal", "v_dot2c dpp quad_perm", "v_dot2 op_sel src1"}; -void FillInput() { - std::uint64_t state = 0x9e3779b97f4a7c15ull; - for (std::uint32_t tid = 0; tid < Threads; ++tid) { - auto* words = &Input[tid * Inputs]; - if (tid < Edges.size()) { - std::copy(Edges[tid].begin(), Edges[tid].end(), words); - continue; - } - words[0] = RandomHalf(state) | (RandomHalf(state) << 16u); - words[1] = RandomHalf(state) | (RandomHalf(state) << 16u); - words[2] = std::bit_cast(Half(RandomHalf(state)) * Half(RandomHalf(state))); - } +void Fill(std::uint32_t tid, std::uint32_t* words) { + for (std::uint32_t i = 0; i < 4; ++i) words[i] = Rows[tid][i]; } std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -77,6 +118,10 @@ std::string Hex(std::uint32_t value) { return text; } +void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("v_dot2_f32_f16: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +} + auto Compile(AgcDriver::VulkanDevice& device, std::span code) { std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); @@ -84,7 +129,7 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span cod std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, @@ -95,14 +140,6 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span cod return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device) { - Output.fill(0xdeadbeefu); - const std::span code(DotCode); - const auto result = Compile(device, code); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); - device.WaitIdle(); -} - void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const std::string& what) { alignas(256) const std::array code{word0, 0x1c1a0b04u, 0xbf810000u}; std::string refusal; @@ -114,30 +151,19 @@ void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const st Require(refusal.find("v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented") != std::string::npos, what + " was not refused"); } -float Dot(float aLow, float aHigh, float bLow, float bHigh, float c) { - return std::fma(aHigh, bHigh, std::fma(aLow, bLow, c)); -} - -void Expect(std::uint32_t tid, std::uint32_t actual, float expected, const char* name) { - const std::uint32_t bits = std::bit_cast(expected); - const bool nan = std::isnan(expected) && std::isnan(std::bit_cast(actual)); - Require(nan || actual == bits, std::string("v_dot2_f32_f16: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(bits)); +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + const std::span code(Code); + const auto result = Compile(device, code); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); } void Check() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const auto* in = &Input[tid * Inputs]; - const auto* out = &Output[tid * Results]; - const float aLow = Half(in[0] & 0xffffu); - const float aHigh = Half(in[0] >> 16u); - const float bLow = Half(in[1] & 0xffffu); - const float bHigh = Half(in[1] >> 16u); - const float c = std::bit_cast(in[2]); - const float plain = Dot(aLow, aHigh, bLow, bHigh, c); - Expect(tid, out[0], plain, "plain"); - Expect(tid, out[1], Dot(aHigh, aLow, bLow, bHigh, c), "op_sel swapping the halves of src0"); - Expect(tid, out[2], Dot(-aLow, aHigh, bLow, -bHigh, -c), "neg_lo src0, neg_hi src1 and neg src2"); - if (!std::isnan(plain)) Expect(tid, out[3], plain > 0.0f ? std::min(plain, 1.0f) : 0.0f, "clamp"); + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 10; ++i) Expect(tid, out[i], Expected[tid][i], Names[i]); } } @@ -147,7 +173,6 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - FillInput(); Run(*device); Check(); CheckRefused(*device, 0xcc13600au, "op_sel on the accumulator"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index b1fac1488..64db626d4 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -261,6 +261,7 @@ enum class IrOpcode : std::uint16_t { FPRsq64, FPSqrt64, FPTrigPreop64, + FPDot2F32F16, ConvertF32F64, ConvertF64F32, ConvertF64S32, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index eb7d22e56..618f3ad0b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -302,6 +302,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("FPRsq64", U64, U64), makeMeta("FPSqrt64", U64, U64), makeMeta("FPTrigPreop64", U64, U64, U32), + makeMeta("FPDot2F32F16", U32, U32, U32, U32), makeMeta("ConvertF32F64", F32, U64), makeMeta("ConvertF64F32", U64, F32), makeMeta("ConvertF64S32", U64, U32), diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index b0e61840a..6080354b9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -195,6 +195,7 @@ std::uint32_t EmitFPRcp64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPRsq64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPSqrt64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPTrigPreop64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2); std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF64F32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF64S32(SpirvEmitterState& state, std::uint32_t arg0); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index c2090afca..1dec262eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -442,6 +442,96 @@ std::uint32_t EmitFPTrigPreop64(SpirvEmitterState& state, std::uint32_t arg0, st return FromF64(state, Exact(state, spv::OpFMul, Exact(state, spv::OpFMul, mantissa, scale), ConstantF64(state, 0x1a70000000000000ull))); } +std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2) { + const auto u32 = TypeU32(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; + const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, TypeBool(state), lhs, ConstantU32(state, value)); }; + const auto logical = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, TypeBool(state), lhs, rhs); }; + const auto differ = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpINotEqual, TypeBool(state), lhs, rhs); }; + const auto pick = [&](std::uint32_t condition, std::uint32_t trueValue, std::uint32_t falseValue) { return Select(state, u32, condition, trueValue, falseValue); }; + struct Half { + std::uint32_t bits = 0; + std::uint32_t sign = 0; + std::uint32_t nan = 0; + std::uint32_t infinite = 0; + std::uint32_t zero = 0; + std::uint32_t value = 0; + }; + const auto half = [&](std::uint32_t packed, std::uint32_t shift) { + Half result; + result.bits = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, packed, shift), 0xffffu); + const auto magnitude = op(spv::OpBitwiseAnd, result.bits, 0x7fffu); + result.sign = op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, result.bits, 0x8000u), 16u); + result.nan = test(spv::OpUGreaterThan, magnitude, 0x7c00u); + result.infinite = test(spv::OpIEqual, magnitude, 0x7c00u); + result.zero = test(spv::OpIEqual, magnitude, 0u); + const auto subnormal = Exact(state, spv::OpFMul, Unary(state, spv::OpConvertUToF, TypeF64(state), magnitude), ConstantF64(state, 0x3e70000000000000ull)); + const auto normal = ToF64(state, Join(state, {ConstantU32(state, 0u), op(spv::OpIAdd, op(spv::OpShiftLeftLogical, magnitude, 10u), 1008u << 20u)})); + const auto unsignedBits = Split(state, FromF64(state, Select(state, TypeF64(state), test(spv::OpULessThan, magnitude, 0x400u), subnormal, normal))); + result.value = ToF64(state, Join(state, {unsignedBits.low, Binary(state, spv::OpBitwiseOr, u32, unsignedBits.high, result.sign)})); + return result; + }; + const auto aLow = half(arg0, 0u); + const auto bLow = half(arg1, 0u); + const auto aHigh = half(arg0, 16u); + const auto bHigh = half(arg1, 16u); + + const auto magnitudeC = op(spv::OpBitwiseAnd, arg2, 0x7fffffffu); + const auto signC = op(spv::OpBitwiseAnd, arg2, 0x80000000u); + const auto nanC = test(spv::OpUGreaterThan, magnitudeC, 0x7f800000u); + const auto infiniteC = test(spv::OpIEqual, magnitudeC, 0x7f800000u); + const auto flushedC = test(spv::OpULessThan, magnitudeC, 0x00800000u); + const auto highC = pick(flushedC, ConstantU32(state, 0u), op(spv::OpIAdd, op(spv::OpShiftRightLogical, magnitudeC, 3u), 896u << 20u)); + const auto lowC = pick(flushedC, ConstantU32(state, 0u), op(spv::OpShiftLeftLogical, magnitudeC, 29u)); + const auto valueC = ToF64(state, Join(state, {lowC, Binary(state, spv::OpBitwiseOr, u32, highC, signC)})); + + const auto productLow = Exact(state, spv::OpFMul, aLow.value, bLow.value); + const auto productHigh = Exact(state, spv::OpFMul, aHigh.value, bHigh.value); + const auto upper = TwoSum(state, productHigh, valueC); + const auto total = TwoSum(state, productLow, upper.high); + const auto sum = Split(state, FromF64(state, AddRoundToOdd(state, total.high, AddRoundToOdd(state, total.low, upper.low)))); + const auto exponent = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, sum.high, 20u), 0x7ffu); + const auto mantissa = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, sum.high, 0x000fffffu), 3u), op(spv::OpShiftRightLogical, sum.low, 29u)); + const auto base = Binary(state, spv::OpIAdd, u32, op(spv::OpShiftLeftLogical, op(spv::OpISub, exponent, 896u), 23u), mantissa); + const auto rest = op(spv::OpBitwiseAnd, sum.low, 0x1fffffffu); + const auto tie = logical(spv::OpLogicalAnd, test(spv::OpIEqual, rest, 0x10000000u), test(spv::OpINotEqual, op(spv::OpBitwiseAnd, base, 1u), 0u)); + const auto roundUp = logical(spv::OpLogicalOr, test(spv::OpUGreaterThan, rest, 0x10000000u), tie); + auto result = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, sum.high, 0x80000000u), Binary(state, spv::OpIAdd, u32, base, pick(roundUp, ConstantU32(state, 1u), ConstantU32(state, 0u)))); + + const auto signLow = Binary(state, spv::OpBitwiseXor, u32, aLow.sign, bLow.sign); + const auto signHigh = Binary(state, spv::OpBitwiseXor, u32, aHigh.sign, bHigh.sign); + const auto zeroSign = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpBitwiseAnd, u32, signLow, signHigh), signC); + const auto zero = test(spv::OpIEqual, Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, sum.high, 0x7fffffffu), sum.low), 0u); + result = pick(zero, zeroSign, result); + + const auto notZero = [&](const Half& value) { return Unary(state, spv::OpLogicalNot, TypeBool(state), value.zero); }; + const auto infiniteProduct = [&](const Half& lhs, const Half& rhs) { + return logical(spv::OpLogicalOr, logical(spv::OpLogicalAnd, lhs.infinite, notZero(rhs)), logical(spv::OpLogicalAnd, rhs.infinite, notZero(lhs))); + }; + const auto invalidProduct = [&](const Half& lhs, const Half& rhs) { + return logical(spv::OpLogicalOr, logical(spv::OpLogicalAnd, lhs.infinite, rhs.zero), logical(spv::OpLogicalAnd, rhs.infinite, lhs.zero)); + }; + const auto infiniteLow = infiniteProduct(aLow, bLow); + const auto infiniteHigh = infiniteProduct(aHigh, bHigh); + auto invalid = logical(spv::OpLogicalOr, invalidProduct(aLow, bLow), invalidProduct(aHigh, bHigh)); + invalid = logical(spv::OpLogicalOr, invalid, logical(spv::OpLogicalAnd, logical(spv::OpLogicalAnd, infiniteLow, infiniteHigh), differ(signLow, signHigh))); + invalid = logical(spv::OpLogicalOr, invalid, logical(spv::OpLogicalAnd, logical(spv::OpLogicalAnd, infiniteLow, infiniteC), differ(signLow, signC))); + invalid = logical(spv::OpLogicalOr, invalid, logical(spv::OpLogicalAnd, logical(spv::OpLogicalAnd, infiniteHigh, infiniteC), differ(signHigh, signC))); + const auto infinite = logical(spv::OpLogicalOr, logical(spv::OpLogicalOr, infiniteLow, infiniteHigh), infiniteC); + const auto infiniteSign = pick(infiniteLow, signLow, pick(infiniteHigh, signHigh, signC)); + result = pick(infinite, op(spv::OpBitwiseOr, infiniteSign, 0x7f800000u), result); + result = pick(invalid, ConstantU32(state, 0xffc00000u), result); + + const auto quietHalf = [&](const Half& value) { + return Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseOr, value.sign, 0x7fc00000u), op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, value.bits, 0x3ffu), 13u)); + }; + result = pick(nanC, op(spv::OpBitwiseOr, arg2, 0x00400000u), result); + result = pick(bHigh.nan, quietHalf(bHigh), result); + result = pick(aHigh.nan, quietHalf(aHigh), result); + result = pick(bLow.nan, quietHalf(bLow), result); + return pick(aLow.nan, quietHalf(aLow), result); +} + std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0) { const auto converted = Unary(state, spv::OpBitcast, TypeU32(state), Unary(state, spv::OpFConvert, TypeF32(state), ToF64(state, arg0))); const auto cls = Classify(state, arg0); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index ca3785c3d..4933cf976 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -466,6 +466,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::FPRsq64: return Invoke(EmitFPRsq64, ctx, inst); case IrOpcode::FPSqrt64: return Invoke(EmitFPSqrt64, ctx, inst); case IrOpcode::FPTrigPreop64: return Invoke(EmitFPTrigPreop64, ctx, inst); + case IrOpcode::FPDot2F32F16: return Invoke(EmitFPDot2F32F16, ctx, inst); case IrOpcode::ConvertF32F64: return Invoke(EmitConvertF32F64, ctx, inst); case IrOpcode::ConvertF64F32: return Invoke(EmitConvertF64F32, ctx, inst); case IrOpcode::ConvertF64S32: return Invoke(EmitConvertF64S32, ctx, inst); diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index e3be70909..95a632087 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -184,6 +184,7 @@ private: bool vDivFixupF64(const RdnaInstruction& inst); bool vDot2cF32F16(const RdnaInstruction& inst); bool vDot2F32F16(const RdnaInstruction& inst); + bool float16Dot2(const RdnaInstruction& inst, const RdnaOperand& lhs, const RdnaOperand& rhs, IrU32 accumulator); bool vCubeidF32(const RdnaInstruction& inst); bool vCubescF32(const RdnaInstruction& inst); bool vCubetcF32(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index e0c73c88d..915c48113 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -522,32 +522,38 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { } bool TranslationContext::vDot2cF32F16(const RdnaInstruction& inst) { - RdnaOperand a = sourceAt(inst, 0u); - a.opSel = false; - a.opSelHi = true; - RdnaOperand b = sourceAt(inst, 1u); - b.opSel = false; - b.opSelHi = true; - const IrF32 aLow = readF16LaneAsF32(a, false); - const IrF32 aHigh = readF16LaneAsF32(a, true); - const IrF32 bLow = readF16LaneAsF32(b, false); - const IrF32 bHigh = readF16LaneAsF32(b, true); - const IrF32 accumulator = readMixF32(inst.destination); - const IrF32 low(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aLow.Value(), &bLow.Value(), &accumulator.Value()})); - const IrF32 result(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aHigh.Value(), &bHigh.Value(), &low.Value()})); - writeOperand(inst.destination, &result.Value()); - return true; + RdnaOperand lhs = sourceAt(inst, 0u); + lhs.opSel = false; + lhs.opSelHi = true; + RdnaOperand rhs = sourceAt(inst, 1u); + rhs.opSel = false; + rhs.opSelHi = true; + return float16Dot2(inst, lhs, rhs, readU32(accumulatorOperand(inst))); } bool TranslationContext::vDot2F32F16(const RdnaInstruction& inst) { - const IrF32 aLow = readF16LaneAsF32(sourceAt(inst, 0u), false, true); - const IrF32 aHigh = readF16LaneAsF32(sourceAt(inst, 0u), true, true); - const IrF32 bLow = readF16LaneAsF32(sourceAt(inst, 1u), false, true); - const IrF32 bHigh = readF16LaneAsF32(sourceAt(inst, 1u), true, true); - IrValue* accumulator = readOperand(sourceAt(inst, 2u), IrType::F32); - const IrF32 low(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aLow.Value(), &bLow.Value(), accumulator})); - const IrF32 result(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aHigh.Value(), &bHigh.Value(), &low.Value()})); - writeOperand(inst.destination, &result.Value()); + return float16Dot2(inst, sourceAt(inst, 0u), sourceAt(inst, 1u), readU32(sourceAt(inst, 2u))); +} + +bool TranslationContext::float16Dot2(const RdnaInstruction& inst, const RdnaOperand& lhs, const RdnaOperand& rhs, IrU32 accumulator) { + const auto packed = [&](const RdnaOperand& operand) { + const IrU32 source = readF16SourceBits(operand); + const auto half = [&](bool highLane) { + const bool selectHigh = highLane ? operand.opSelHi : operand.opSel; + IrU32 bits(selectHigh ? ir.ShiftRightLogical(source.Value(), ir.Constant(16u)) : ir.BitwiseAnd(source.Value(), ir.Constant(0xffffu))); + if (highLane ? operand.negateHi : operand.negate) { + bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); + } + return bits; + }; + return IrU32(ir.BitwiseOr(half(false).Value(), ir.ShiftLeftLogical(half(true).Value(), ir.Constant(16u)))); + }; + const IrU32 a = packed(lhs); + const IrU32 b = packed(rhs); + const IrU32 result(ir.Emit(IrOpcode::FPDot2F32F16, IrType::U32, {&a.Value(), &b.Value(), &accumulator.Value()})); + RdnaOperand destination = inst.destination; + destination.clamp = false; + writeOperand(destination, &ir.BitCastF32(result.Value())); return true; } From ca2cbc29a3edfbc3232d6012a7b174be564cc1f5 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:34:57 +0300 Subject: [PATCH 06/43] style(pad): remove disallowed explanatory comment --- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index cf4ec4491..ab3aa11a8 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -188,7 +188,6 @@ PadInputState PadInput::sampleController() const { result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); } } - // View has no touch position, so use a neutral center contact when SDL reports none. if (viewPressed && !result.touch[0].active && !result.touch[1].active) { result.touch[0] = {true, 960, 471, 0}; } From 67d2c3c0a3d9ccf82b510de9839669b47703b4ce Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:05:50 +0200 Subject: [PATCH 07/43] feat(userservice): implement sceUserServiceGetAgeLevel Report age level 0 for the initial user and reject other users and a null output like the other user queries. The meaning of the value is unknown and recorded in the technical debt; PPSA12544 only stores it for its user profile. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceUserService/Export.cpp | 9 +++++---- docs/dev/TechnicalDebt.md | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 1695f4546..2da7ff360 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -49,10 +49,11 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zo } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - (void)user_id; - (void)age_level; - NotImplemented_nid_no_patch(__func__); - return 0; + if (age_level == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *age_level = 0; + return USER_SERVICE_OK; } // The initial user is reported as logging in once; afterwards there are no user events. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a84ead929..d8a1f4b86 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -72,6 +72,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sce::Np::CppWebApi::Common::String](../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws for a non-null buffer: releasing it needs the `LibContext` allocator, which is not implemented - [sceNpTrophy2UnregisterUnlockCallback](../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) - unknown signature +- [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature From 8f6328242d8c19c5ffdfd45bb23d3db6a1e0b76d Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 18:41:52 +0200 Subject: [PATCH 08/43] feat(libs): add exports imported by PPSA12544 Implement scePthreadGetschedparam and scePthreadSetschedparam on top of the thread priority, POSIX utimes and fsync on top of sceKernelUtimes and sceKernelFsync, and sceUserServiceGetAccessibilityZoomFollowFocus like the other accessibility queries. Add the remaining imports of PPSA12544 as unimplemented exports and record unknown NIDs and signatures in the technical debt. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Unimplemented.cpp | 12 ++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 10 +++++ core/libs/prx/libSceHttp2/Export.cpp | 20 +++++++++ core/libs/prx/libSceNet/Export.cpp | 6 +++ core/libs/prx/libScePad/Export.cpp | 6 +++ core/libs/prx/libSceSsl/Export.cpp | 30 +++++++++++++ core/libs/prx/libSceSystemService/Export.cpp | 25 +++++++++++ core/libs/prx/libSceUserService/Export.cpp | 8 ++++ core/libs/prx/libc/src/HeapDiagnostics.cpp | 18 ++++++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 10 +++++ .../prx/libkernel/Pthread/Posix/Thread.cpp | 13 ++++++ core/libs/prx/libkernel/Unimplemented.cpp | 42 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 5 +++ 13 files changed, 205 insertions(+) diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index e53a08cb3..3e405bd60 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -150,4 +150,16 @@ int APS5_VABI sceAgcUnknown_y5K5tPktiL8() { return 0; } +APS5_EXPORT("dbOlWdppb4o", sceAgcUnknown_dbOlWdppb4o); +int APS5_VABI sceAgcUnknown_dbOlWdppb4o(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); +int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index c1b833af5..56e6eb21b 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -457,6 +457,16 @@ int APS5_VABI sceFontWritingLineWritesOrder() { return 0; } +int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 911b65730..adaa00e78 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -214,4 +214,24 @@ int APS5_VABI sceHttp2GetMemoryPoolStats() { return 0; } +int APS5_VABI sceHttp2CookieFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2CreateCookieBox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieBox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetRequestNoContentLength(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 19a4b22bd..456301ccf 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -911,4 +911,10 @@ int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, } return 0; } + +int APS5_VABI sceNetResolverGetError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index f2437a5a5..adc42a6b7 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -233,4 +233,10 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enab return PAD_OK; } +APS5_EXPORT("fCWdlnmB1Ks", scePadUnknown_fCWdlnmB1Ks); +int APS5_VABI scePadUnknown_fCWdlnmB1Ks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 4cca48fc7..5ef04eacb 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -73,4 +73,34 @@ int APS5_VABI sceSslGetMemoryPoolStats() { return 0; } +int APS5_VABI sceSslFreeSslCertName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetIssuerName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetNameEntryCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetNameEntryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetPem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetSubjectName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 01a6365a5..faef48204 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -130,4 +130,29 @@ int APS5_VABI sceSystemServiceLaunchPlayerDialog(const void* param) { return SYSTEM_SERVICE_OK; } +int APS5_VABI sceSystemServiceDisableMusicPlayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenableMusicPlayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowControllerSettings(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 1695f4546..1ded3463e 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -48,6 +48,14 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zo return USER_SERVICE_OK; } +int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t* zoom_follow_focus) { + if (zoom_follow_focus == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *zoom_follow_focus = 0; + return USER_SERVICE_OK; +} + int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { (void)user_id; (void)age_level; diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 0288d5c0a..16910f8a1 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -48,4 +48,22 @@ int32_t APS5_VABI sceLibcUnknown_BnMAMrsfVWo(void) { return 0; } +APS5_EXPORT("OQ-dzhlnM28", sceLibcInternalExtUnknown_OQ_dzhlnM28); +int APS5_VABI sceLibcInternalExtUnknown_OQ_dzhlnM28(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("qBS714-Jr3g", sceLibcInternalExtUnknown_qBS714_Jr3g); +int APS5_VABI sceLibcInternalExtUnknown_qBS714_Jr3g(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("tB59hFLH3SA", sceLibcInternalExtUnknown_tB59hFLH3SA); +int APS5_VABI sceLibcInternalExtUnknown_tB59hFLH3SA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index cf4c3a305..4d597c117 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -487,4 +487,14 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* return 0; } +int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (const int error = PathError(path)) return PosixFailure(error); + return PosixResult(sceKernelUtimes_nid_postfix(path, times)); +} + +int APS5_VABI fsync_nid_postfix(int fd) { + if (sceKernelFsync(fd) != 0) return PosixFailure(errno); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index fc9fd94d8..b2281ac47 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "../include/Pthread.hpp" #include "Common.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include extern "C" { @@ -39,6 +40,18 @@ void APS5_VABI pthread_exit_nid_postfix(void* value) { scePthreadExit(value); } +int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param) { + if (!policy || !param) return SCE_KERNEL_ERROR_EINVAL; + *policy = GUEST_SCHED_FIFO; + return scePthreadGetprio(thread, ¶m->sched_priority); +} + +int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param) { + (void)policy; + if (!param) return SCE_KERNEL_ERROR_EINVAL; + return scePthreadSetprio(thread, param->sched_priority); +} + int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param) { if (!policy || !param) return PosixThread::GUEST_EINVAL; *policy = GUEST_SCHED_FIFO; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index c969fc55e..025f33969 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -30,4 +30,46 @@ int APS5_VABI sceKernelUnknown02(void) { NotImplemented_nid_no_patch("0D4-FVvEikw"); return 0; } +APS5_EXPORT("B2n8aDorSH4", sceKernelUnknown_B2n8aDorSH4); +int APS5_VABI sceKernelUnknown_B2n8aDorSH4(void) { + NotImplemented_nid_no_patch("B2n8aDorSH4"); + return 0; +} + +APS5_EXPORT("NH6xARDOVv8", sceKernelUnknown_NH6xARDOVv8); +int APS5_VABI sceKernelUnknown_NH6xARDOVv8(void) { + NotImplemented_nid_no_patch("NH6xARDOVv8"); + return 0; +} + +APS5_EXPORT("PZQhiiLXRFs", sceKernelUnknown_PZQhiiLXRFs); +int APS5_VABI sceKernelUnknown_PZQhiiLXRFs(void) { + NotImplemented_nid_no_patch("PZQhiiLXRFs"); + return 0; +} + +int APS5_VABI sceKernelFchmod(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI fchmod_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI futimes_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI recvmsg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sendmsg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 02ea84a84..058a342c8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -60,6 +60,11 @@ Throughout the project, every function at every stage either **does exactly what - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures +- [dbOlWdppb4o, vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature +- [OQ-dzhlnM28, qBS714-Jr3g, tB59hFLH3SA](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternalExt, implemented in libc) - unknown name, signature +- [fCWdlnmB1Ks](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown name, signature +- [B2n8aDorSH4, NH6xARDOVv8, PZQhiiLXRFs](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap From 4a548c0ba5739c7e26d89e95f0ef19b4e7de2cfc Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:02:21 +0200 Subject: [PATCH 09/43] feat(libkernel): implement sceKernelGetOperationMode NH6xARDOVv8 is the NID of sceKernelGetOperationMode. PPSA12544 passes two int outputs and logs them as mode and submode; both are reported as 0. B2n8aDorSH4 and PZQhiiLXRFs are sceKernelSyncOnAddressWait32 and sceKernelSyncOnAddressWait64, implemented by #633, and are no longer added as unimplemented exports. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Process.cpp | 8 ++++++++ core/libs/prx/libkernel/Unimplemented.cpp | 18 ------------------ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 7755b1543..a3838db51 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -321,4 +321,12 @@ int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } +int APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { + if (!mode || !submode) + throw std::invalid_argument("sceKernelGetOperationMode: null output"); + *mode = 0; + *submode = 0; + return 0; +} + } diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 025f33969..64e5093db 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -30,24 +30,6 @@ int APS5_VABI sceKernelUnknown02(void) { NotImplemented_nid_no_patch("0D4-FVvEikw"); return 0; } -APS5_EXPORT("B2n8aDorSH4", sceKernelUnknown_B2n8aDorSH4); -int APS5_VABI sceKernelUnknown_B2n8aDorSH4(void) { - NotImplemented_nid_no_patch("B2n8aDorSH4"); - return 0; -} - -APS5_EXPORT("NH6xARDOVv8", sceKernelUnknown_NH6xARDOVv8); -int APS5_VABI sceKernelUnknown_NH6xARDOVv8(void) { - NotImplemented_nid_no_patch("NH6xARDOVv8"); - return 0; -} - -APS5_EXPORT("PZQhiiLXRFs", sceKernelUnknown_PZQhiiLXRFs); -int APS5_VABI sceKernelUnknown_PZQhiiLXRFs(void) { - NotImplemented_nid_no_patch("PZQhiiLXRFs"); - return 0; -} - int APS5_VABI sceKernelFchmod(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 058a342c8..bf025de38 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -63,7 +63,7 @@ Throughout the project, every function at every stage either **does exactly what - [dbOlWdppb4o, vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature - [OQ-dzhlnM28, qBS714-Jr3g, tB59hFLH3SA](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternalExt, implemented in libc) - unknown name, signature - [fCWdlnmB1Ks](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown name, signature -- [B2n8aDorSH4, NH6xARDOVv8, PZQhiiLXRFs](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` From 845e4dd9427f35c9c09d69e100599ed45e0c76cb Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:26:15 +0200 Subject: [PATCH 10/43] fix(libs): drop exports implemented elsewhere OQ-dzhlnM28, qBS714-Jr3g and tB59hFLH3SA are _sceLibcInternalThreadDtors, _sceLibcInternalThreadAtexit and _sceLibcInternalForceTlsDestructor, implemented in libSceLibcInternal by #641. dbOlWdppb4o is implemented by #651. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Unimplemented.cpp | 6 ------ core/libs/prx/libc/src/HeapDiagnostics.cpp | 18 ------------------ docs/dev/TechnicalDebt.md | 3 +-- 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 3e405bd60..a541d3e22 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -150,12 +150,6 @@ int APS5_VABI sceAgcUnknown_y5K5tPktiL8() { return 0; } -APS5_EXPORT("dbOlWdppb4o", sceAgcUnknown_dbOlWdppb4o); -int APS5_VABI sceAgcUnknown_dbOlWdppb4o(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 16910f8a1..0288d5c0a 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -48,22 +48,4 @@ int32_t APS5_VABI sceLibcUnknown_BnMAMrsfVWo(void) { return 0; } -APS5_EXPORT("OQ-dzhlnM28", sceLibcInternalExtUnknown_OQ_dzhlnM28); -int APS5_VABI sceLibcInternalExtUnknown_OQ_dzhlnM28(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -APS5_EXPORT("qBS714-Jr3g", sceLibcInternalExtUnknown_qBS714_Jr3g); -int APS5_VABI sceLibcInternalExtUnknown_qBS714_Jr3g(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -APS5_EXPORT("tB59hFLH3SA", sceLibcInternalExtUnknown_tB59hFLH3SA); -int APS5_VABI sceLibcInternalExtUnknown_tB59hFLH3SA(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index bf025de38..1a1b88b93 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -60,8 +60,7 @@ Throughout the project, every function at every stage either **does exactly what - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures -- [dbOlWdppb4o, vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature -- [OQ-dzhlnM28, qBS714-Jr3g, tB59hFLH3SA](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternalExt, implemented in libc) - unknown name, signature +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature - [fCWdlnmB1Ks](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown name, signature - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures From de93a9492ca5a27b4e72320b0e7ad1ea5bee28ef Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:41:47 +0200 Subject: [PATCH 11/43] feat(libkernel): load the relinked module for a guest module path dlopen and sceKernelLoadStartModule passed the guest path to the host loader, which cannot load the original ELF module. When a relinked module .guest.prx exists beside it, that module is loaded instead; for a module the executable already loaded at startup the host loader returns the same module. PPSA12544 loads /app0/Media/Modules/Il2CppUserAssemblies.prx at startup and resolves IL2CPP functions through it. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Module/src/DynamicLoader.cpp | 12 ++++++++++-- core/libs/tests/GuestDynamicLoader.cpp | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index e1091def8..ef4790b78 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include #include #include #include @@ -57,6 +58,13 @@ char* APS5_VABI dlerror_nid_postfix() { pendingError = false; return loaderError.data(); } +static std::filesystem::path RelinkedModulePath(const std::filesystem::path& path) { + auto relinked = path; + relinked += ".guest.prx"; + std::error_code error; + return std::filesystem::is_regular_file(relinked, error) ? relinked : path; +} + void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { if ((flags & ~0x103) || (flags & 3) == 0 || (flags & 3) == 3) { Error("dlopen: unsupported flags"); return nullptr; @@ -70,7 +78,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { module->owned = false; } else { if (!*path) { Error("dlopen: empty module path"); return nullptr; } - const auto resolved = ResolvePath_nid_no_patch(path); + const auto resolved = RelinkedModulePath(ResolvePath_nid_no_patch(path)); module->native = LoadLibraryExW(resolved.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); } if (!module->native) { @@ -79,7 +87,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { Error(message); return nullptr; } #else - const auto resolved = path ? ResolvePath_nid_no_patch(path).string() : std::string{}; + const auto resolved = path ? RelinkedModulePath(ResolvePath_nid_no_patch(path)).string() : std::string{}; const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 2fa27e917..04b369b73 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include extern "C" { void* APS5_VABI dlopen_nid_postfix(const char*, int); @@ -37,4 +39,20 @@ int main(int argc, char** argv) { Require(add && add(2, 3) == 5); Require(dlclose_nid_postfix(second) == 0); Require(dlclose_nid_postfix(second) == -1); + + const std::filesystem::path guest = "anyps5-relinked-module-for-test.prx"; + auto relinked = guest; + relinked += ".guest.prx"; + { + std::ofstream elf(guest, std::ios::binary); + elf.write("\x7f" "ELF", 4); + } + std::filesystem::copy_file(argv[1], relinked, std::filesystem::copy_options::overwrite_existing); + void* redirected = dlopen_nid_postfix(guest.string().c_str(), 2); + Require(redirected != nullptr); + add = reinterpret_cast(dlsym_nid_postfix(redirected, "GuestModuleAdd")); + Require(add && add(40, 2) == 42); + Require(dlclose_nid_postfix(redirected) == 0); + std::filesystem::remove(guest); + std::filesystem::remove(relinked); } From d7f3b7c494fc7cfe0a6797468607b8570a8a2b2b Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 20:09:25 +0200 Subject: [PATCH 12/43] feat(voiceqos): implement sceVoiceQoSInit A title passes a work memory block and app type 0x20000000, and turns voice chat off when initialization fails. Initialization now records the library as initialized and succeeds. A null or empty memory block, another app type and a second initialization throw, as their error codes are unknown. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVoiceQoS/CMakeLists.txt | 21 +++++++++++++++ core/libs/prx/libSceVoiceQoS/Export.cpp | 23 ++++++++++++---- core/libs/tests/GuestVoiceQoS.cpp | 30 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceVoiceQoS/CMakeLists.txt create mode 100644 core/libs/tests/GuestVoiceQoS.cpp diff --git a/core/libs/prx/libSceVoiceQoS/CMakeLists.txt b/core/libs/prx/libSceVoiceQoS/CMakeLists.txt new file mode 100644 index 000000000..29d56e9ad --- /dev/null +++ b/core/libs/prx/libSceVoiceQoS/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceVoiceQoS SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVoiceQoS PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVoiceQoS) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_voice_qos_tests ${LIBS_INCLUDE_DIR}/tests/GuestVoiceQoS.cpp) + target_include_directories(guest_voice_qos_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_voice_qos_tests PRIVATE libSceVoiceQoS libc) + configure_windows_unwind(guest_voice_qos_tests) + add_test(NAME guest_voice_qos COMMAND guest_voice_qos_tests) + if(WIN32) + set_tests_properties(guest_voice_qos PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index a0af71ff3..725088af0 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,16 +1,29 @@ #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; + +std::mutex g_mutex; +bool g_initialized = false; + +} + extern "C" { int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_type) { - (void)mem_block; - (void)mem_size; - (void)app_type; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!mem_block || mem_size == 0) throw std::invalid_argument("sceVoiceQoSInit: null or empty memory block"); + if (app_type != VOICE_QOS_APP_TYPE_GAME) throw std::invalid_argument("sceVoiceQoSInit: unsupported app type " + std::to_string(app_type)); + std::lock_guard lock(g_mutex); + if (g_initialized) throw std::runtime_error("sceVoiceQoSInit: already initialized"); + g_initialized = true; + return 0; } } diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp new file mode 100644 index 000000000..9f9e668a6 --- /dev/null +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -0,0 +1,30 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool Throws(TFunction function) { + try { + function(); + } catch (const TException&) { + return true; + } + return false; +} + +int main() { + std::vector memory(0x40000); + Require(Throws([&] { sceVoiceQoSInit(nullptr, 0x40000, 0x20000000); })); + Require(Throws([&] { sceVoiceQoSInit(memory.data(), 0, 0x20000000); })); + Require(Throws([&] { sceVoiceQoSInit(memory.data(), 0x40000, 0); })); + Require(sceVoiceQoSInit(memory.data(), 0x40000, 0x20000000) == 0); + Require(Throws([&] { sceVoiceQoSInit(memory.data(), 0x40000, 0x20000000); })); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 02ea84a84..a501121c5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -59,6 +59,7 @@ Throughout the project, every function at every stage either **does exactly what - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask +- [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - the error codes are unknown: a null or empty memory block, an app type other than 0x20000000 (the value PPSA14632 passes) and a second initialization throw. The minimum memory size is unknown and the block is not used, as no endpoint is implemented - AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` From 3f2e5150c26cc5716e5e765fa2361ce6ccf956f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:15:28 +0200 Subject: [PATCH 13/43] feat(libc): implement _Xtime_get_ticks Co-Authored-By: Claude --- core/libs/prx/libc/Export.cpp | 6 ------ core/libs/prx/libc/src/Time.cpp | 13 +++++++++++++ core/libs/tests/GuestFreebsdClocks.cpp | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index 17128de2e..01f9abc13 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -106,12 +106,6 @@ std::uint64_t APS5_VABI libcCyberUnknown05(void) { return 0; } -APS5_EXPORT("Cj+Fw5q1tUo", libcCyberUnknown06); -std::uint64_t APS5_VABI libcCyberUnknown06(void) { - NotImplemented_nid_no_patch("Cj+Fw5q1tUo"); - return 0; -} - APS5_EXPORT("CyXs2l-1kNA", libcCyberUnknown07); std::uint64_t APS5_VABI libcCyberUnknown07(void) { NotImplemented_nid_no_patch("CyXs2l-1kNA"); diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 64bc20ddb..53199eed7 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -21,6 +21,19 @@ int64_t APS5_VABI time_nid_postfix(int64_t* timer) { return libc_time_nid_postfix(timer); } +int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { +#ifdef _WIN32 + FILETIME ft{}; + GetSystemTimePreciseAsFileTime(&ft); + const uint64_t t = ((static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime) - 116444736000000000ULL; + return static_cast(t / 10); +#else + timespec now{}; + clock_gettime(CLOCK_REALTIME, &now); + return static_cast(now.tv_sec) * 1000000 + now.tv_nsec / 1000; +#endif +} + double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0) { return std::difftime(static_cast(time1), static_cast(time0)); } diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 6c7f959f0..75a69daa6 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -8,6 +8,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); +std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); } static constexpr int SCE_OK = 0; @@ -120,6 +122,21 @@ static void IdleProcessAccumulatesNone() { Require(Nanos(GUEST_CLOCK_PROF) - profStart <= IDLE_LIMIT_NANOS); } +static std::int64_t Microseconds() { + KernelTimeval time{-1, -1}; + Require(gettimeofday_nid_postfix(&time, nullptr) == SCE_OK); + return time.tv_sec * 1000000LL + time.tv_usec; +} + +static void XtimeTicksAreGettimeofdayMicroseconds() { + for (int i = 0; i < 100; ++i) { + const std::int64_t before = Microseconds(); + const std::int64_t ticks = _Xtime_get_ticks_nid_postfix(); + const std::int64_t after = Microseconds(); + Require(before <= ticks && ticks <= after); + } +} + int main() { SecondClockReportsWholeSeconds(); @@ -135,4 +152,5 @@ int main() { ProcessClocksAreOrdered(); BusyProcessAccumulatesUserTime(); IdleProcessAccumulatesNone(); + XtimeTicksAreGettimeofdayMicroseconds(); } From ee2b3be52fc9d5456d568a05115fb28a27ef096c Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 20:21:11 +0200 Subject: [PATCH 14/43] fix(relinker): allow duplicate Linux guest exports that no module imports Guest modules a title opens with sceKernelLoadStartModule are looked up through their own handle, so several modules may export the same name: every Wwise plugin exports g_pAKPluginList. On Linux a duplicate now fails only when the executable or a guest module imports it, as the provider would be ambiguous. Windows keeps rejecting duplicates. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 26 ++++++++++++++++++- .../tests/test_guest_module_directories.py | 16 +++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index eebb504ca..cef7ff2cf 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -49,6 +49,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; std::map exports; + std::map> sharedExports; std::set outputNames; Io::FileReader reader; for (const auto& path : paths) { @@ -65,7 +66,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; const auto [existing, inserted] = exports.emplace(symbol.Name, images.size()); - if (!inserted) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + images.at(existing->second).SourcePath.string() + " and " + path.string()); + if (inserted) continue; + if (windows || existing->second == images.size()) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + images.at(existing->second).SourcePath.string() + " and " + path.string()); + auto& providers = sharedExports[symbol.Name]; + providers.insert(existing->second); + providers.insert(images.size()); } std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); @@ -88,6 +93,24 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!inserted && found->second != index) throw Domain::RelinkerException("Ambiguous guest dependency name: " + name); } } + const auto rejectSharedImport = [&](const std::string& name, const std::string& importer) { + const auto shared = sharedExports.find(name); + if (shared == sharedExports.end()) return; + std::string providers; + for (const auto provider : shared->second) providers += " " + images[provider].SourcePath.string(); + throw Domain::RelinkerException("Ambiguous guest import " + name + " in " + importer + ": exported by" + providers); + }; + if (dynamic.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid executable symbol table"); + for (std::size_t offset = 0; offset < dynamic.DynSymData.size(); offset += 24) { + if (Io::ReadU16(dynamic.DynSymData, offset + 6) != 0) continue; + const auto nameOffset = Io::ReadU32(dynamic.DynSymData, offset); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid executable symbol name offset"); + const auto start = dynamic.DynStrData.begin() + nameOffset; + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated executable symbol name"); + const std::string name(start, end); + rejectSharedImport(name.substr(0, name.find('#')), inputPath.string()); + } std::vector> dependencies(images.size()); for (auto& image : images) image.UsePlatformTlsResolver = !exports.contains("vNe1w4diLCs"); for (std::size_t index = 0; index < images.size(); ++index) { @@ -97,6 +120,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (const auto& symbol : images[index].Symbols) { if (symbol.Section != 0 || symbol.Name.empty()) continue; + rejectSharedImport(symbol.Name, images[index].SourcePath.string()); const auto found = exports.find(symbol.Name); if (found != exports.end()) { const auto& provider = images[found->second]; diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index 5d0389cb2..992f89abf 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -122,7 +122,21 @@ def main(): (case / name).mkdir(parents=True) (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) result, output = convert(case, windows) - assert result.returncode == 2 and "Duplicate guest export" in result.stderr, result.stderr + if windows: + assert result.returncode == 2 and "Duplicate guest export" in result.stderr, result.stderr + assert not output.exists() and not (case / "app0").exists(), output + else: + assert result.returncode == 0 and output.exists(), result.stderr + assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"sce_module.prx.guest.prx", "prx.prx.guest.prx"} + + case = work / f"{windows}-ambiguous-import" + for name in ("sce_module", "prx"): + (case / name).mkdir(parents=True) + (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) + (case / "prx" / "consumer.prx").write_bytes(module_with_symbol(False)) + result, output = convert(case, windows) + expected = "Duplicate guest export" if windows else "Ambiguous guest import shared" + assert result.returncode == 2 and expected in result.stderr, result.stderr assert not output.exists() and not (case / "app0").exists(), output print("Guest module directory integration tests passed") From b9c1c8848f415cfdae2d04b8bc1e6144860b20f6 Mon Sep 17 00:00:00 2001 From: Cliff Syner Date: Mon, 5 Oct 2026 14:23:12 +0000 Subject: [PATCH 15/43] feat(libc): implement strnlen_s The DQbtGaBKlaw import was a throwing stub (libcCyberUnknown17). It is the NID of "strnlen_s", which C11 K.3.7.4.4 defines as strnlen that returns 0 for a null pointer. The removed stub comment from #358 described its call site as float -> float, which contradicts the hash-verified name; #492 left the stub untouched for that reason. --- core/libs/prx/libc/Export.cpp | 9 --------- core/libs/prx/libc/src/StringExtensions.cpp | 4 ++++ core/libs/tests/GuestStrings.cpp | 3 +++ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index 17128de2e..66df4093f 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -44,15 +44,6 @@ int APS5_VABI libcUnknown_u2tMGOLaqnE() { return 0; } -// Live float->float import of Cyberpunk 2077 (unnamed); callers pass one -// float in xmm0 and consume the result, so return 0.0f instead of integer 0. -APS5_EXPORT("DQbtGaBKlaw", libcCyberUnknown17); -float APS5_VABI libcCyberUnknown17(float x) { - (void)x; - NotImplemented_nid_no_patch("DQbtGaBKlaw"); - return 0.0f; -} - // Live Cyberpunk 2077 import used in an fopen/fseek/ftell-like file-size // idiom as (handle, 0, 2); returning 0 reports success. APS5_EXPORT("rWSuTWY2JN0", libcCyberUnknown18); diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index 9533fd26b..a9a759763 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -26,6 +26,10 @@ std::size_t APS5_VABI strnlen_nid_postfix(const char* text, std::size_t limit) { return length; } +std::size_t APS5_VABI strnlen_s_nid_postfix(const char* text, std::size_t limit) { + return text ? strnlen_nid_postfix(text, limit) : 0; +} + char* APS5_VABI strncat_nid_postfix(char* destination, const char* source, std::size_t limit) { return std::strncat(destination, source, limit); } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index a743df2ac..808f69407 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -8,6 +8,7 @@ extern "C" { char* APS5_VABI basename_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); +std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); @@ -88,6 +89,8 @@ int main() { Require(strnlen_nid_postfix(bounded, 0) == 0); Require(strnlen_nid_postfix(bounded, sizeof(bounded)) == 3); Require(strnlen_nid_postfix("a", 8) == 1); + Require(strnlen_s_nid_postfix(nullptr, 8) == 0); + Require(strnlen_s_nid_postfix("abc", 8) == 3 && strnlen_s_nid_postfix("abcdef", 4) == 4); char truncated[] = "abXX"; Require(strlcat_nid_postfix(truncated, "cd", 2) == 4); Require(std::strcmp(truncated, "abXX") == 0); From 55f548d6bcf3cc8c1bc632d4604a52cb901dc1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:28:41 +0200 Subject: [PATCH 16/43] feat(libc): implement wcsrtombs_s Co-Authored-By: Claude --- core/libs/prx/libc/CMakeLists.txt | 9 ++++ core/libs/prx/libc/Export.cpp | 6 --- core/libs/prx/libc/src/Multibyte.cpp | 73 ++++++++++++++++++++++++++ core/libs/tests/GuestWcsrtombsS.cpp | 78 ++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 core/libs/tests/GuestWcsrtombsS.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 070d85afa..f5736578e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -176,6 +176,15 @@ if(BUILD_TESTING) set_tests_properties(guest_strings PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wcsrtombs_s_tests ${LIBS_INCLUDE_DIR}/tests/GuestWcsrtombsS.cpp) + target_include_directories(guest_wcsrtombs_s_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wcsrtombs_s_tests PRIVATE libc) + configure_windows_unwind(guest_wcsrtombs_s_tests) + add_test(NAME guest_wcsrtombs_s COMMAND guest_wcsrtombs_s_tests) + if(WIN32) + set_tests_properties(guest_wcsrtombs_s PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(UNIX) add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index 17128de2e..0c1fcb59f 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -94,12 +94,6 @@ std::uint64_t APS5_VABI libcCyberUnknown03(void) { return 0; } -APS5_EXPORT("79s2tnYQI6I", libcCyberUnknown04); -std::uint64_t APS5_VABI libcCyberUnknown04(void) { - NotImplemented_nid_no_patch("79s2tnYQI6I"); - return 0; -} - APS5_EXPORT("7yMFgcS8EPA", libcCyberUnknown05); std::uint64_t APS5_VABI libcCyberUnknown05(void) { NotImplemented_nid_no_patch("7yMFgcS8EPA"); diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 0ea78acb3..c521ced22 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { @@ -47,6 +48,78 @@ std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value return 1; } +int APS5_VABI wcsrtombs_s_nid_postfix( + std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, + void* state +) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr int GuestEilseq = 86; + constexpr auto Failed = static_cast(-1); + if (!result || !source || !state || !*source || (destination && (static_cast(capacity) <= 0 + || static_cast(limit) < 0))) { + if (destination && static_cast(capacity) > 0) destination[0] = '\0'; + if (result) *result = Failed; + return GuestEinval; + } + if (!destination && capacity) { + *result = Failed; + return GuestEinval; + } + const auto* wide = *source; + char bytes[MB_LEN_MAX]; + if (!destination) { + std::size_t total = 0; + for (;; ++wide) { + const auto length = wcrtomb_nid_postfix(bytes, *wide, state); + if (length == Failed) { + *result = Failed; + return GuestEilseq; + } + if (length && bytes[length - 1] == '\0') { + *result = total + length - 1; + return 0; + } + total += length; + } + } + std::size_t written = 0; + while (limit) { + const auto length = wcrtomb_nid_postfix(bytes, *wide, state); + if (length == Failed) { + destination[written] = '\0'; + *result = Failed; + return GuestEilseq; + } + if (length > limit || length > capacity) { + *source = wide; + destination[0] = '\0'; + *result = Failed; + return GuestErange; + } + std::memcpy(destination + written, bytes, length); + written += length; + if (length && destination[written - 1] == '\0') { + *source = nullptr; + *result = written - 1; + return 0; + } + ++wide; + capacity -= length; + if (!capacity) { + *source = wide; + destination[0] = '\0'; + *result = Failed; + return GuestErange; + } + limit -= length; + } + *source = wide; + destination[written] = '\0'; + *result = written; + return 0; +} + std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { if (!destination) return std::strlen(*source); std::size_t converted = 0; diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp new file mode 100644 index 000000000..758ff5d33 --- /dev/null +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" int APS5_VABI wcsrtombs_s_nid_postfix( + std::size_t*, char*, std::size_t, const std::uint16_t**, std::size_t, void*); +static void Require(bool value) { if (!value) std::abort(); } + +constexpr auto Failed = static_cast(-1); +constexpr auto Huge = std::size_t{1} << 63; +static const std::uint16_t text[] = {'a', 'b', 'c', 0}; +static const std::uint16_t wide[] = {'a', 0x100, 'b', 0}; + +struct Run { + int status; + std::size_t result = 7; + const std::uint16_t* source; + char out[8]; +}; + +static Run Convert(const std::uint16_t* input, std::size_t capacity, std::size_t limit, bool toBuffer = true) { + Run run{}; + std::memset(run.out, 'x', sizeof(run.out)); + run.source = input; + std::uint64_t state[2] = {}; + run.status = wcsrtombs_s_nid_postfix(&run.result, toBuffer ? run.out : nullptr, capacity, &run.source, limit, state); + return run; +} + +int main() { + auto run = Convert(text, 0, 0, false); + Require(run.status == 0 && run.result == 3 && run.source == text); + run = Convert(wide, 0, Huge, false); + Require(run.status == 86 && run.result == Failed && run.source == wide); + run = Convert(text, 5, 1, false); + Require(run.status == 22 && run.result == Failed && run.source == text); + + run = Convert(text, 8, 8); + Require(run.status == 0 && run.result == 3 && run.source == nullptr && std::memcmp(run.out, "abc\0x", 5) == 0); + run = Convert(text, 4, 4); + Require(run.status == 0 && run.result == 3 && run.source == nullptr && std::memcmp(run.out, "abc\0x", 5) == 0); + run = Convert(text, 8, 2); + Require(run.status == 0 && run.result == 2 && run.source == text + 2 && std::memcmp(run.out, "ab\0x", 4) == 0); + run = Convert(text, 8, 0); + Require(run.status == 0 && run.result == 0 && run.source == text && std::memcmp(run.out, "\0x", 2) == 0); + + run = Convert(text, 3, 8); + Require(run.status == 34 && run.result == Failed && run.source == text + 3 && std::memcmp(run.out, "\0bcx", 4) == 0); + run = Convert(text, 3, 3); + Require(run.status == 34 && run.result == Failed && run.source == text + 3 && std::memcmp(run.out, "\0bcx", 4) == 0); + run = Convert(text, 1, 8); + Require(run.status == 34 && run.result == Failed && run.source == text + 1 && std::memcmp(run.out, "\0x", 2) == 0); + + run = Convert(wide, 8, 8); + Require(run.status == 86 && run.result == Failed && run.source == wide && std::memcmp(run.out, "a\0xx", 4) == 0); + + run = Convert(text, 0, 8); + Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == 'x'); + run = Convert(text, Huge, 8); + Require(run.status == 22 && run.result == Failed && run.out[0] == 'x'); + run = Convert(text, 8, Huge); + Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == '\0'); + run = Convert(nullptr, 8, 8); + Require(run.status == 22 && run.result == Failed && run.out[0] == '\0'); + + char out[4] = {'x', 'x', 'x', 'x'}; + std::size_t result = 7; + const std::uint16_t* source = text; + std::uint64_t state[2] = {}; + Require(wcsrtombs_s_nid_postfix(nullptr, out, 4, &source, 4, state) == 22 && out[0] == '\0' && source == text); + out[0] = 'x'; + Require(wcsrtombs_s_nid_postfix(&result, out, 4, nullptr, 4, state) == 22 && result == Failed && out[0] == '\0'); + out[0] = 'x'; + result = 7; + Require(wcsrtombs_s_nid_postfix(&result, out, 4, &source, 4, nullptr) == 22 && result == Failed && out[0] == '\0'); + Require(source == text && std::memcmp(out, "\0xxx", 4) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 02ea84a84..c178995dd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -104,4 +104,5 @@ Throughout the project, every function at every stage either **does exactly what - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. +- [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped From 124be3e7e80eecb415104df2d85569a819e28af0 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Mon, 5 Oct 2026 21:02:47 +0200 Subject: [PATCH 17/43] feat(agc-driver): execute boolean SET_PREDICATION SET_PREDICATION threw, and packets with the predicate header bit were rejected. The executor now keeps the queue's predication: operations 3 and 4 read a 64- or 32-bit value and bit 8 selects whether predicated packets run when it is non-zero or zero; operation 0 clears it. Before a predicated packet runs while predication is active, queued labels are recorded and the value is read, as for COND_EXEC; the packet is skipped when the predicate fails. Indirect buffers are inlined when a submission is copied, so a predicated INDIRECT_BUFFER call keeps its header in front of the inlined commands and records their end like a COND_EXEC range; the block runs or is skipped on one evaluation, as on the command processor. Query predication, a predicated COND_EXEC, predicated chains and flips inside predicated command buffers throw. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/include/Pm4.hpp | 2 + .../Execution/include/QueueState.hpp | 7 ++ .../src/Driver/Packets/PacketExecution.cpp | 11 +++ .../src/Driver/Queues/Submission.cpp | 13 +++ .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 36 ++++++- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 95 ++++++++++++++++++- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 4 +- docs/dev/TechnicalDebt.md | 1 + 8 files changed, 159 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index c413e357e..31f780e61 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -88,6 +88,8 @@ bool AccessesMemory(std::uint32_t header); // synchronization): such a packet needs a pipeline barrier, not a device drain. bool UsesGpuCacheBarrier(std::span packet); bool IsTagMarker(std::span packet); +inline bool Predicated(std::uint32_t header) { return !FillerPacket(header) && (header & 1u) != 0; } +bool PredicationPasses(const QueueState& queue); bool WaitSatisfied(std::span packet); // WaitSatisfied for a polling loop: the caller has validated the address once, so the value is read // directly instead of through the checked guest memory path. diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 6d18f6228..19493fc5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -167,6 +167,12 @@ inline Registers InitialContextRegisters() { return result; } +struct Predication { + std::uint64_t address = 0; + std::uint32_t operation = 0; + bool executeWhenSet = false; +}; + struct QueueState { Registers shader; Registers context = InitialContextRegisters(); @@ -180,6 +186,7 @@ struct QueueState { std::uint32_t indexType = 0; std::uint32_t instanceCount = 1; std::vector markers; + Predication predication; void ClearContext() { context = InitialContextRegisters(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 06dcb02cb..3c5dcd148 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -117,6 +117,17 @@ void Driver::execute(const Submission& submission) { GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue); CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast(submission.serial), submission.queue, cursor, header, packet.size()); + if (Pm4::Predicated(header) && queue.predication.operation != 0) { + recordQueuedLabelsBeforeRead(submission.queue); + if (!Pm4::PredicationPasses(queue)) { + cursor = opcode == 0x3f ? submission.conditionalEnds.at(cursor) : nextCursor; + continue; + } + } + if (opcode == 0x3f) { + cursor = nextCursor; + continue; + } const auto flushStart = profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; flushBetweenPackets(submission.queue, header, opcode == 0x49 || opcode == 0x37); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index edd9b5b70..cdc6d68b8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -50,6 +50,19 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std const bool chain = (guest[cursor + 3] & (1u << 20u)) != 0; require(!chain || guarded.empty(), "a chained INDIRECT_BUFFER inside a conditional execution range is not implemented"); GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t)); + if (Pm4::Predicated(header)) { + require(!chain, "predicated command buffer chains are not implemented"); + const auto start = submission.commands.size(); + submission.commands.insert(submission.commands.end(), guest + cursor, guest + cursor + count); + require(!copySegment(submission, target, targetWords, budget), "a REWIND inside a predicated command buffer is not implemented"); + for (auto inner = start + count; inner < submission.commands.size(); inner += Pm4::PacketWords(submission.commands[inner])) { + const auto innerHeader = submission.commands[inner]; + require(innerHeader != FlipPacketHeader && innerHeader != RenderingWaitPacketHeader, "flips and rendering waits in predicated command buffers are not implemented"); + } + submission.conditionalEnds.emplace(start, submission.commands.size()); + cursor += count; + continue; + } if (copySegment(submission, target, targetWords, budget)) { require(guarded.empty(), "a REWIND inside a conditional execution range is not implemented"); return true; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index a07acee1a..26f818d92 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -166,7 +166,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { } } switch (opcode) { - case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x22: case 0x26: case 0x27: + case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27: case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x46: case 0x50: case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: case 0x83: case 0x9f: return {}; @@ -175,7 +175,6 @@ std::string_view UnsupportedReason(std::uint32_t header) { return {}; 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 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented"; case 0x3c: case 0x93: return {}; case 0x39: @@ -213,7 +212,7 @@ void Validate(std::span packet, std::uint32_t queue) { 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 (!reason.empty() && !(opcode == 0x3f && Predicated(header))) throw std::runtime_error(std::string(reason)); if (opcode == 0x10) { require((header & 3u) == 0, "unsupported NOP header flags"); switch ((header >> 2u) & 0x3fu) { @@ -239,11 +238,11 @@ void Validate(std::span packet, std::uint32_t queue) { return; } // Header bit 1 selects the compute shader type and bit 2 (register writes) resets the filter CAM; - // neither changes what the packet writes. Predication (bit 0) is not implemented. + // neither changes what the packet writes. const auto flags = header & 0xffu; const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a; auto allowedFlags = opcode == 0x11 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u; - if (IsTagMarker(packet)) allowedFlags |= 1u; + if (opcode != 0x20 && opcode != 0x22) allowedFlags |= 1u; if ((flags & ~allowedFlags) != 0) throw std::runtime_error("PM4 header flags 0x" + ToHex(flags) + " are not implemented"); switch (opcode) { case 0x11: @@ -252,6 +251,17 @@ void Validate(std::span packet, std::uint32_t queue) { if ((header & 2u) == 0) graphics(); break; case 0x12: graphics(); size(2); require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits"); break; + case 0x20: { + graphics(); + size(4); + const auto operation = (packet[1] >> 16u) & 7u; + require(operation != 1 && operation != 2, "GPU query predication is not implemented"); + require(operation == 0 || operation == 3 || operation == 4, "invalid predication operation"); + require((packet[1] & ~0x71100u) == 0, "unsupported SET_PREDICATION bits"); + if (operation != 0) require(address(packet[2], packet[3]) != 0 && (packet[2] & 0xfu) == 0, "unaligned predication address"); + break; + } + case 0x3f: size(4); break; case 0x13: case 0x2f: graphics(); size(2); break; case 0x26: graphics(); size(3); break; case 0x27: @@ -476,6 +486,16 @@ std::size_t ConditionalWords(std::span packet) { return packet[4] & ConditionalWordsMask; } +bool PredicationPasses(const QueueState& queue) { + const auto& predication = queue.predication; + if (predication.operation == 0) return true; + const std::size_t bytes = predication.operation == 3 ? 8 : 4; + std::uint64_t value = 0; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait); + GuestMemory::Read(predication.address, std::as_writable_bytes(std::span(&value, 1)).first(bytes), bytes); + return (value != 0) == predication.executeWhenSet; +} + std::uint32_t ReadCondition(std::span packet) { require(packet.size() == 5 && ((packet[0] >> 8u) & 0xffu) == 0x22u, "expected COND_EXEC packet"); std::uint32_t value = 0; @@ -725,6 +745,12 @@ void Execute(std::span packet, QueueState& queue) { APS5_LOG_OUT_DEBUG("CLEAR_STATE targetMaskBefore=0x%x shaderMaskBefore=0x%x", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u); queue.ClearContext(); return; case 0x13: queue.indexBufferSize = packet[1]; return; + case 0x20: { + const auto operation = (packet[1] >> 16u) & 7u; + queue.predication = {operation == 0 ? 0 : address(packet[2], packet[3]), operation, (packet[1] & 0x100u) != 0}; + return; + } + case 0x3f: throw std::runtime_error(std::string(UnsupportedReason(packet[0]))); case 0x26: queue.indexBase = address(packet[1], packet[2]); return; case 0x2a: queue.indexType = packet[1]; return; case 0x2f: queue.instanceCount = packet[1]; return; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 2efbe1ab5..3af51bbd8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -180,7 +180,7 @@ void testAutoDraw() { } 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}, 1), 0); }, "header flags"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 2), 0); }, "header flags"); state.userConfig[0x24a] = std::numeric_limits::max(); check(AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {1, 2}), state).firstVertex == std::numeric_limits::max(), "last vertex rejected"); expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {2, 2}), state); }, "vertex range overflow"); @@ -508,7 +508,7 @@ void testEventWrite() { 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}, 1), 0); }, "header flags"); + 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"); @@ -541,7 +541,7 @@ void testAcquireMem() { packet[index] = value; expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, reason); }; - invalidWord(0, captured[0] | 1u, "header flags"); + invalidWord(0, captured[0] | 2u, "header flags"); invalidWord(1, 4, "control flags"); invalidWord(1, 0x00800000, "control flags"); invalidWord(3, 1, "above 40 bits"); @@ -554,6 +554,50 @@ void testAcquireMem() { 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(&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(&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(&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 source{0x10, 73}; std::array destination{}; @@ -693,6 +737,49 @@ void testConditionalSubmission() { check(results[13] == 0, "a rejected conditional submission executed a guarded packet"); } +void testPredicatedSubmission() { + alignas(16) std::uint64_t flag[2] = {0, 0}; + alignas(16) std::array written{}; + alignas(16) std::array 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(nested.size())}, 1); + const auto submit = [](std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "predicated submission failed"); + AgcDriverWaitIdle_nid_postfix(); + }; + std::vector 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(nested.size())}, 1); + Packet chained{chain.data(), static_cast(chain.size()), 0, {}}; + expectFailure([&] { sceAgcDriverSubmitDcb(&chained); }, "predicated command buffer chains"); + AgcDriverWaitIdle_nid_postfix(); +} + void testAsyncMemoryFailure() { auto commands = makePacket(0x37, {0x100, 0x1000, 0, 1}); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; @@ -728,7 +815,9 @@ int main(int argc, char** argv) { testConditionReadSynchronization(); testEventWrite(); testAcquireMem(); + testPredication(); testDriverSubmission(); + testPredicatedSubmission(); testConditionalSubmission(); LibcRunShutdown_nid_postfix(); std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index e2b8d991f..28a8f081a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -76,7 +76,7 @@ void testValidation() { expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); commands[0] = 0xc001105c; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); - commands[0] = 0xc0017601; + commands[0] = 0xc0017608; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); commands[0] = 0xc0017600; commands[1] = 0x10000; @@ -110,7 +110,7 @@ void testClearState() { words[0] = 0xc0011200; packet.dw_num = 3; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); - words[0] = 0xc0001201; + words[0] = 0xc0001202; packet.dw_num = 2; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); words[0] = 0xc0001200; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 92cad4624..09bf9eb24 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -33,6 +33,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted +- [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [qj7QZpgr9Uw](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name From cefdea7d14b57ed7eef12e24d66ea050ea9b730c Mon Sep 17 00:00:00 2001 From: Jakub Krasuski Date: Mon, 5 Oct 2026 21:41:12 +0200 Subject: [PATCH 18/43] fix(libkernel): report EINVAL, EACCES, ENOBUFS, ETIMEDOUT and EINTR from Linux sockets The Linux branch of NativeError lacked five codes the Windows branch already translates, so they fell through to EIO. bind on a bound socket and accept on a socket that is not listening set errno to 5 on Linux and to 22 on Windows. Assisted-by: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++++ core/libs/tests/GuestTcp.cpp | 2 ++ 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5462087a4..bffca9598 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -57,6 +57,7 @@ int NativeError() { case EAGAIN: return 35; case EADDRINUSE: return 48; case EADDRNOTAVAIL: return 49; + case EACCES: return 13; case EMSGSIZE: return 40; case ENETUNREACH: return 51; case EHOSTUNREACH: return 65; @@ -65,6 +66,10 @@ int NativeError() { case ECONNABORTED: return 53; case EISCONN: return 56; case ENOTCONN: return 57; + case ENOBUFS: return 55; + case ETIMEDOUT: return 60; + case EINTR: return 4; + case EINVAL: return 22; default: return 5; } #endif diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 2c6927336..0b9b42f2a 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -27,6 +27,7 @@ int main() { Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + Require(bind_nid_postfix(listener, address.data(), address.size()) == -1 && *__error_nid_postfix() == 22); Require(listen_nid_postfix(listener, 4) == 0); std::uint32_t address_size = address.size(); Require(getsockname_nid_postfix(listener, address.data(), &address_size) == 0); @@ -34,6 +35,7 @@ int main() { const int client = socket_nid_postfix(2, 1, 0); Require(client >= 0); + Require(accept_nid_postfix(client, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22); Require(connect_nid_postfix(client, address.data(), address.size()) == 0); std::array peer{}; address_size = peer.size(); From 01220faa1a1b8bb65c81a0f553f8f3d83f85ebbd Mon Sep 17 00:00:00 2001 From: Jakub Krasuski Date: Mon, 5 Oct 2026 21:56:50 +0200 Subject: [PATCH 19/43] fix(net): keep SIGPIPE from killing the process on Linux sends sceNetSend and sceNetSendto called send and sendto without MSG_NOSIGNAL, so a send on a connection the peer had closed raised SIGPIPE and terminated the process. shadPS4 has sceNetSend pass 0x20000, FreeBSD's MSG_NOSIGNAL, down to sendto, so a title gets the error back instead of a signal. Pass MSG_NOSIGNAL on Linux and translate EPIPE, which fell through to EIO. Assisted-by: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 9 ++++++--- core/libs/tests/GuestSceNet.cpp | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 19a4b22bd..bc226d10b 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -65,6 +65,7 @@ constexpr int NET_SO_NBIO = 0x1200; using NativeSocket = SOCKET; using NativeLength = int; constexpr NativeSocket INVALID_NATIVE_SOCKET = INVALID_SOCKET; +constexpr int NATIVE_SEND_FLAGS = 0; bool initialize_sockets() { static const int result = [] { WSADATA data{}; @@ -95,6 +96,7 @@ int native_error() { using NativeSocket = int; using NativeLength = socklen_t; constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; +constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } int native_error() { @@ -115,6 +117,7 @@ int native_error() { case EINTR: return 4; case EINVAL: return NET_EINVAL; case ENOTSOCK: return NET_ENOTSOCK; + case EPIPE: return 32; default: return 5; } } @@ -484,7 +487,7 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), 0); + const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); return result >= 0 ? result : fail(native_error()); } @@ -502,13 +505,13 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), 0); + result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), 0, + result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS, reinterpret_cast(&destination), destination_length); } return result >= 0 ? result : fail(native_error()); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index aae168ecc..eda6e5954 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1,9 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" #include +#include #include #include #include +#include extern "C" { int APS5_VABI sceNetInit_nid_postfix(void); @@ -71,6 +73,12 @@ int main() { Require(std::strcmp(request, response) == 0); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); + bool send_failed = false; + for (int attempt = 0; attempt < 100 && !send_failed; ++attempt) { + send_failed = sceNetSend(client, request, sizeof(request), 0) < 0; + if (!send_failed) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(send_failed); Require(sceNetSocketClose(client) == 0); Require(sceNetSocketClose(listener) == 0); From f91e13447d08a741503f635722d2befe34208471 Mon Sep 17 00:00:00 2001 From: Jakub Krasuski Date: Mon, 5 Oct 2026 21:57:55 +0200 Subject: [PATCH 20/43] fix(net): return SCE_NET_ERROR codes from failing sceNet calls Every failing sceNet function returned -1 and left the code only in sceNetErrnoLoc. The functions return the errno combined with 0x80410100 (SCE_NET_ERROR_*), as the resolver paths of this file already do with RESOLVER_ENODNS and as shadPS4 and fpPS4 do for the whole library. A title that compares the result with SCE_NET_ERROR_EWOULDBLOCK saw a non-blocking receive without data as an unknown failure. Assisted-by: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 3 ++- core/libs/tests/GuestSceNet.cpp | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 19a4b22bd..7c906c033 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -49,6 +49,7 @@ constexpr int NET_ECONNABORTED = 53; constexpr int NET_EMSGSIZE = 40; constexpr int NET_ETIMEDOUT = 60; constexpr int NET_ECONNREFUSED = 61; +constexpr int NET_ERROR_BASE = static_cast(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); constexpr int NET_AF_INET = 2; @@ -212,7 +213,7 @@ int* errno_slot() { int fail(int err) { *errno_slot() = err; - return -1; + return NET_ERROR_BASE | err; } void log_soft(const char* func, const char* what) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index aae168ecc..0b5deea90 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -18,6 +18,8 @@ std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int* APS5_VABI sceNetErrnoLoc(void); int APS5_VABI sceNetEpollCreate(const char*, int); int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); @@ -53,6 +55,10 @@ int main() { address_size = peer.size(); const int accepted = sceNetAccept(listener, peer.data(), &address_size); Require(accepted >= 0 && address_size == 16); + const int nonblocking = 1; + Require(sceNetSetsockopt(accepted, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + char pending = 0; + Require(sceNetRecv(accepted, &pending, sizeof(pending), 0) == static_cast(0x80410123) && *sceNetErrnoLoc() == 35); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); @@ -73,6 +79,7 @@ int main() { Require(sceNetSocketClose(accepted) == 0); Require(sceNetSocketClose(client) == 0); Require(sceNetSocketClose(listener) == 0); + Require(sceNetSocketClose(listener) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); const int udp_receiver = sceNetSocket(nullptr, 2, 2, 17); const int udp_sender = sceNetSocket(nullptr, 2, 2, 17); From 636fb5edae5d972da56990daf83a4b43488e37a2 Mon Sep 17 00:00:00 2001 From: Chad Date: Mon, 5 Oct 2026 15:02:30 -0500 Subject: [PATCH 21/43] feat(libs): implement the AMPR 04_00 counter commands and the construct helpers sceAmprCommandBufferWaitOnCounter_04_00 and WriteCounter_04_00 take the counter access size and offset, the wait mask and the write operation, with their measure functions; ConstructNop records a nop with an optional leading word and a payload of up to 60 bytes, and ConstructMarker records the set, push and pop markers by type. Signatures and checks follow ampr_emu. The executor now reads and writes counters by access: the 8-byte access of counter n spans n and n + 1, the 2- and 1-byte accesses a half or a byte of the counter. Waits compare at the access width, so the signed and wrapped compares see the field's own sign bit, and the atomic writes wrap within the field. The legacy counter commands record a 4-byte access. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAmpr/Export.cpp | 68 ++++++++--- .../Apr/include/AprCommandBuffer.hpp | 28 ++++- core/libs/prx/libkernel/Apr/src/Apr.cpp | 63 +++++++++- core/libs/tests/GuestAmpr.cpp | 114 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 248 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index df54ef647..f5bb93e89 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -36,6 +36,14 @@ static bool ValidWait(std::uint32_t compare, std::uint32_t flush) { return compare < 4u && flush < 2u; } +static bool ValidWaitOnCounter_04_00(std::uint32_t access, std::uint32_t compare, std::uint32_t maskOperation, std::uint32_t flush) { + return access < 8u && compare <= 6u && maskOperation < 2u && flush < 2u; +} + +static bool ValidWriteCounter_04_00(std::uint32_t counter, std::uint32_t access, std::uint32_t operation) { + return (counter & 0x80u) == 0u && access < 8u && operation < 5u; +} + static constexpr std::uint64_t MeasureInvalid = static_cast(SCE_KERNEL_ERROR_EINVAL); static std::uint64_t NopBytes(std::uint32_t dwords) { @@ -88,7 +96,7 @@ int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObj int APS5_VABI sceAmprCommandBufferWriteCounterOnCompletion(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint32_t value) { if (!ValidCounter(counter)) return SCE_KERNEL_ERROR_EINVAL; - return AppendCommand(buffer, Apr::Opcode::WriteCounter, Apr::WriteCounterCommand{{}, counter, value}); + return AppendCommand(buffer, Apr::Opcode::WriteCounter, Apr::WriteCounterCommand{{}, counter, Apr::CounterAccess::Size4, Apr::CounterOperation::Store, 0, value}); } int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t reference, std::uint8_t compare, std::uint8_t flush) { @@ -98,7 +106,7 @@ int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject* buffer int APS5_VABI sceAmprCommandBufferWaitOnCounter(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint32_t reference, std::uint8_t compare, std::uint8_t flush) { if (!ValidCounter(counter) || !ValidWait(compare, flush)) return SCE_KERNEL_ERROR_EINVAL; - return AppendCommand(buffer, Apr::Opcode::WaitOnCounter, Apr::WaitCommand{{}, 0, reference, ~0ull, counter, compare}); + return AppendCommand(buffer, Apr::Opcode::WaitOnCounter, Apr::WaitCommand{{}, 0, reference, ~0ull, counter, compare, Apr::CounterAccess::Size4, 0}); } int APS5_VABI sceAmprCommandBufferWriteKernelEventQueueOnCompletion(Apr::CommandBufferObject* buffer, std::uint64_t equeue, std::int32_t ident, std::uint64_t data) { @@ -196,14 +204,32 @@ int APS5_VABI sceAmprCommandBufferClearBuffer(Apr::CommandBufferObject* buffer) return 0; } -int APS5_VABI sceAmprCommandBufferConstructMarker() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject* buffer, std::uint32_t type, const char* text, const std::uint32_t* color) { + switch (type) { + case 1: return AppendMarker(buffer, Apr::Opcode::SetMarker, text); + case 2: return AppendMarker(buffer, Apr::Opcode::PushMarker, text); + case 3: return AppendCommand(buffer, Apr::Opcode::PopMarker, Apr::MarkerCommand{}); + case 5: return color ? AppendMarker(buffer, Apr::Opcode::SetMarker, text) : SCE_KERNEL_ERROR_EINVAL; + case 6: return color ? AppendMarker(buffer, Apr::Opcode::PushMarker, text) : SCE_KERNEL_ERROR_EINVAL; + default: return SCE_KERNEL_ERROR_EINVAL; + } } -int APS5_VABI sceAmprCommandBufferConstructNop() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, std::int16_t type, const void* payload, std::uint32_t payloadBytes, const std::uint32_t* word) { + (void)type; + if (payloadBytes > (word ? 56u : 60u)) return SCE_KERNEL_ERROR_EINVAL; + if (payloadBytes != 0u && !payload) throw std::invalid_argument("sceAmprCommandBufferConstructNop: null payload"); + const std::uint32_t dwords = (payloadBytes + 3u) / 4u + (word ? 1u : 0u); + const std::uint32_t offset = buffer ? buffer->offset : 0u; + const int result = AppendNop(buffer, dwords, nullptr); + if (result != 0) return result; + std::uint8_t* data = buffer->base + offset + sizeof(Apr::CommandHeader); + if (word) { + std::memcpy(data, word, sizeof(*word)); + data += sizeof(*word); + } + if (payloadBytes != 0u) std::memcpy(data, payload, payloadBytes); + return 0; } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { @@ -288,9 +314,10 @@ int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00(Apr::CommandBufferObject* return AppendCommand(buffer, Apr::Opcode::WaitOnAddress, Apr::WaitCommand{{}, reinterpret_cast(address), reference, ~0ull, 0, compare}); } -int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint8_t access, std::uint64_t reference, std::uint8_t compare, std::uint8_t maskOperation, std::uint64_t mask, std::uint8_t flush) { + if (!ValidWaitOnCounter_04_00(access, compare, maskOperation, flush)) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t applied = maskOperation ? mask : ~0ull; + return AppendCommand(buffer, Apr::Opcode::WaitOnCounter, Apr::WaitCommand{{}, 0, reference, applied, counter, compare, static_cast(access), 0}); } int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint8_t counter, std::uint64_t atStart) { @@ -317,9 +344,10 @@ int APS5_VABI sceAmprCommandBufferWriteAddress_04_00(Apr::CommandBufferObject* b return Append(buffer, &command, sizeof(command)); } -int APS5_VABI sceAmprCommandBufferWriteCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteCounter_04_00(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint8_t access, std::uint64_t value, std::uint8_t operation, std::uint8_t atStart) { + (void)atStart; + if (!ValidWriteCounter_04_00(counter, access, operation)) return SCE_KERNEL_ERROR_EINVAL; + return AppendCommand(buffer, Apr::Opcode::WriteCounter, Apr::WriteCounterCommand{{}, counter, static_cast(access), static_cast(operation), 0, value}); } int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00(Apr::CommandBufferObject* buffer, std::uint64_t equeue, std::int32_t ident, std::uint64_t data, std::uint64_t atStart) { @@ -403,9 +431,9 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00(volatile st return sizeof(Apr::WaitCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00(std::uint8_t, std::uint8_t access, std::uint64_t, std::uint8_t compare, std::uint8_t maskOperation, std::uint64_t, std::uint8_t flush) { + if (!ValidWaitOnCounter_04_00(access, compare, maskOperation, flush)) return MeasureInvalid; + return sizeof(Apr::WaitCommand); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t* address, std::uint8_t counter) { @@ -428,9 +456,9 @@ uint32_t APS5_VABI sceAmprMeasureCommandSizeWriteAddress_04_00(void) { return sizeof(Apr::WriteAddressCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00(std::uint8_t counter, std::uint8_t access, std::uint64_t, std::uint8_t operation) { + if (!ValidWriteCounter_04_00(counter, access, operation)) return MeasureInvalid; + return sizeof(Apr::WriteCounterCommand); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00(std::uint64_t, std::int32_t, std::uint64_t) { diff --git a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index ecdbd67e6..1ac5932a8 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -60,10 +60,32 @@ struct WriteAddressCommand { std::uint32_t reserved; }; +enum class CounterAccess : std::uint8_t { + Size8 = 0, + Size4 = 1, + Size2Offset0 = 2, + Size2Offset1 = 3, + Size1Offset0 = 4, + Size1Offset1 = 5, + Size1Offset2 = 6, + Size1Offset3 = 7, +}; + +enum class CounterOperation : std::uint8_t { + Store = 0, + AtomicOr = 1, + AtomicAndComplement = 2, + AtomicXor = 3, + AtomicAdd = 4, +}; + struct WriteCounterCommand { CommandHeader header; std::uint32_t counter; - std::uint32_t value; + CounterAccess access; + CounterOperation operation; + std::uint16_t reserved; + std::uint64_t value; }; struct WaitCommand { @@ -72,7 +94,9 @@ struct WaitCommand { std::uint64_t reference; std::uint64_t mask; std::uint32_t counter; - std::uint32_t compare; + std::uint8_t compare; + CounterAccess access; + std::uint16_t reserved; }; struct WriteKernelEventQueueCommand { diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 9152f9419..79ab4fde2 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -161,10 +161,63 @@ void _writeAddress(const Apr::WriteAddressCommand& command) { std::array, 256> g_counters{}; +std::mutex g_counterWrites; + std::uint32_t _counter(std::uint32_t index) { return g_counters[index % g_counters.size()].load(std::memory_order_acquire); } +struct CounterField { + std::uint32_t bits; + std::uint32_t shift; +}; + +CounterField _counterField(Apr::CounterAccess access) { + const auto value = static_cast(access); + if (value == 0) return {64, 0}; + if (value == 1) return {32, 0}; + if (value < 4) return {16, (value - 2) * 16}; + if (value < 8) return {8, (value - 4) * 8}; + throw std::runtime_error("APR: counter access " + std::to_string(value) + " not implemented"); +} + +std::uint64_t _fieldMask(std::uint32_t bits) { + return bits == 64 ? ~std::uint64_t{0} : (std::uint64_t{1} << bits) - 1u; +} + +std::uint64_t _readCounter(std::uint32_t index, Apr::CounterAccess access) { + const auto field = _counterField(access); + if (field.bits == 64) return _counter(index) | static_cast(_counter(index + 1u)) << 32u; + return (_counter(index) >> field.shift) & _fieldMask(field.bits); +} + +std::uint64_t _applyCounterOperation(Apr::CounterOperation operation, std::uint64_t current, std::uint64_t value) { + switch (operation) { + case Apr::CounterOperation::Store: return value; + case Apr::CounterOperation::AtomicOr: return current | value; + case Apr::CounterOperation::AtomicAndComplement: return current & ~value; + case Apr::CounterOperation::AtomicXor: return current ^ value; + case Apr::CounterOperation::AtomicAdd: return current + value; + } + throw std::runtime_error("APR: counter operation " + std::to_string(static_cast(operation)) + " not implemented"); +} + +void _writeCounter(const Apr::WriteCounterCommand& command) { + const auto field = _counterField(command.access); + const std::lock_guard lock(g_counterWrites); + const std::uint64_t current = _readCounter(command.counter, command.access); + const std::uint64_t next = _applyCounterOperation(command.operation, current, command.value) & _fieldMask(field.bits); + auto& low = g_counters[command.counter % g_counters.size()]; + if (field.bits == 64) { + g_counters[(command.counter + 1u) % g_counters.size()].store(static_cast(next >> 32u), std::memory_order_release); + low.store(static_cast(next), std::memory_order_release); + return; + } + const auto mask = static_cast(_fieldMask(field.bits) << field.shift); + const auto bits = static_cast(next << field.shift); + low.store((low.load(std::memory_order_relaxed) & ~mask) | bits, std::memory_order_release); +} + bool _waitSatisfied(std::uint32_t compare, std::uint64_t value, std::uint64_t reference) { constexpr std::uint64_t sign = std::uint64_t{1} << 63u; switch (compare) { @@ -212,18 +265,20 @@ void _execute(const Apr::CommandBufferObject& buffer) { break; } case Apr::Opcode::WriteCounter: { - const auto command = _read(buffer, cursor); - g_counters[command.counter % g_counters.size()].store(command.value, std::memory_order_release); + _writeCounter(_read(buffer, cursor)); break; } case Apr::Opcode::WaitOnAddress: case Apr::Opcode::WaitOnCounter: { const auto command = _read(buffer, cursor); + const bool counter = header.opcode == Apr::Opcode::WaitOnCounter; + const std::uint32_t unused = counter ? 64u - _counterField(command.access).bits : 0u; const auto current = [&]() -> std::uint64_t { - if (header.opcode == Apr::Opcode::WaitOnCounter) return _counter(command.counter); + if (counter) return _readCounter(command.counter, command.access); return std::atomic_ref(*reinterpret_cast(command.address)).load(std::memory_order_acquire); }; - while (!_waitSatisfied(command.compare, current() & command.mask, command.reference & command.mask)) std::this_thread::sleep_for(std::chrono::microseconds(50)); + const std::uint64_t reference = (command.reference & command.mask) << unused; + while (!_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) std::this_thread::sleep_for(std::chrono::microseconds(50)); break; } case Apr::Opcode::WriteKernelEventQueue: { diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 80b105261..b2ff45a58 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -7,6 +7,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject*); @@ -46,6 +47,12 @@ int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00(Apr::CommandBuffer std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(volatile std::uint64_t*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(volatile std::uint64_t*, std::uint8_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t*, std::uint8_t); +int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00(Apr::CommandBufferObject*, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); +int APS5_VABI sceAmprCommandBufferWriteCounter_04_00(Apr::CommandBufferObject*, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00(std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00(std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); +int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject*, std::int16_t, const void*, std::uint32_t, const std::uint32_t*); +int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject*, std::uint32_t, const char*, const std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } @@ -288,6 +295,111 @@ void TestVersionedCommands() { Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 10) == sizeof(Apr::WriteAddressFromCounterCommand)); } +void SubmitWithin10Seconds(const Recorder& recorder) { + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0); }); + Require(submitted.wait_for(std::chrono::seconds(10)) == std::future_status::ready); + Require(submitted.get() == 0); +} + +void TestVersionedCounters() { + enum : std::uint8_t { size8, size4, size2Offset0, size2Offset1, size1Offset0, size1Offset1, size1Offset2, size1Offset3 }; + enum : std::uint8_t { store, atomicOr, atomicAndComplement, atomicXor, atomicAdd }; + Recorder recorder; + alignas(8) std::uint64_t fields = 0; + alignas(8) std::uint64_t wide = 0; + alignas(8) std::uint64_t bits = 0; + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 20, size4, 0x11223344u, store, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 20, size1Offset2, 0x1AAu, store, 1) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 20, size2Offset0, 0xFFFFu, atomicAdd, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 22, size8, 0x0000000500000001ull, store, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 22, size8, 0xFFFFFFFFu, atomicAdd, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0xF0u, store, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0x0Fu, atomicOr, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0x3Cu, atomicAndComplement, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0xFFu, atomicXor, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size1Offset3, 0x11u, 0, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size1Offset2, 1u, 6, 0, 0, 1) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size2Offset0, 0xF000u, 4, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size2Offset1, 0x11ABu, 2, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 22, size8, 0x0000000600000000ull, 0, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 24, size4, 0xFCu, 0, 1, 0x0Fu, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 24, size1Offset0, 0x3Cu, 0, 0, 0x0Fu, 0) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterOnCompletion(&recorder.buffer, &fields, 20) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterPairOnCompletion(&recorder.buffer, &wide, 22) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterOnCompletion(&recorder.buffer, &bits, 24) == 0); + Require(recorder.Commands() == 19); + SubmitWithin10Seconds(recorder); + Require(fields == 0x11AA3343u && wide == 0x0000000600000000ull && bits == 0x3Cu); + + const auto rejected = static_cast(static_cast(invalidArgument)); + Recorder empty; + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, 8, 0, 0, 0, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, size4, 0, 7, 0, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, size4, 0, 0, 2, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, size4, 0, 0, 0, 0, 2) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(&empty.buffer, 128, size4, 0, store, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(&empty.buffer, 0, 8, 0, store, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(&empty.buffer, 0, size4, 0, 5, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(nullptr, 0, size4, 0, store, 0) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(200, size1Offset3, 0, 6, 1, 0, 1) == sizeof(Apr::WaitCommand)); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, 8, 0, 0, 0, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, size4, 0, 7, 0, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, size4, 0, 0, 2, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, size4, 0, 0, 0, 0, 2) == rejected); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(127, size8, 0, atomicAdd) == sizeof(Apr::WriteCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(128, size4, 0, store) == rejected); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(0, 8, 0, store) == rejected); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(0, size4, 0, 5) == rejected); +} + +void TestConstructed() { + Recorder recorder; + const std::uint8_t payload[5] = {1, 2, 3, 4, 5}; + const std::uint32_t word = 0xCAFEF00Du; + auto offset = recorder.Offset(); + auto commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 7, payload, sizeof(payload), &word) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); + const std::uint8_t* data = recorder.memory.data() + offset + sizeof(Apr::CommandHeader); + const std::uint8_t padding[3] = {}; + Require(std::memcmp(data, &word, sizeof(word)) == 0 && std::memcmp(data + 4, payload, sizeof(payload)) == 0 && std::memcmp(data + 9, padding, sizeof(padding)) == 0); + + const std::array large{}; + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0, large.data(), 60, nullptr) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(16)); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0, nullptr, 0, nullptr) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(1)); + + const std::pair markers[] = {{1, Apr::Opcode::SetMarker}, {2, Apr::Opcode::PushMarker}, {5, Apr::Opcode::SetMarker}, {6, Apr::Opcode::PushMarker}}; + for (const auto& [type, opcode] : markers) { + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructMarker(&recorder.buffer, type, "stream", &color) == 0); + RequireRecorded(recorder, offset, commands, opcode, sceAmprMeasureCommandSizeSetMarker("stream"), "stream"); + } + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructMarker(&recorder.buffer, 3, nullptr, nullptr) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::PopMarker, sceAmprMeasureCommandSizePopMarker()); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + + Recorder empty; + Require(sceAmprCommandBufferConstructNop(&empty.buffer, 0, large.data(), 61, nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructNop(&empty.buffer, 0, large.data(), 57, &word) == invalidArgument); + Require(sceAmprCommandBufferConstructNop(nullptr, 0, large.data(), 4, nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 5, "stream", nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 6, "stream", nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 1, nullptr, nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 0, "stream", &color) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 4, "stream", &color) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); +} + } int main() { @@ -304,5 +416,7 @@ int main() { TestRejectedWaitsAndCounters(); TestNops(); TestVersionedCommands(); + TestVersionedCounters(); + TestConstructed(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8122ef8fa..5615df0f0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -33,6 +33,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR counter access](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` From 4b336335a8f7832e660edb6d0b896ad010a694e0 Mon Sep 17 00:00:00 2001 From: Chad Date: Mon, 5 Oct 2026 15:12:34 -0500 Subject: [PATCH 22/43] feat(libs): implement the APR gather and scatter reads sceAmprAprCommandBufferReadFileGather, ReadFileScatter, ReadFileGatherScatter and ResetGatherScatterState, with their measure functions. Signatures, argument checks and the read cursor follow ampr_emu: each read leaves the file, the next offset and the next destination; a gather reads at a new offset into the next destination, a scatter continues in the file into a new destination, and a gather-scatter takes both. Recording one needs a file read since the last reset, tracked in the command buffer object next to its type. sceAmprAprCommandBufferReadFile now applies the same length, offset and destination checks and rejects a null command buffer instead of dereferencing it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAmpr/Export.cpp | 86 ++++++++++------ .../Apr/include/AprCommandBuffer.hpp | 13 ++- core/libs/prx/libkernel/Apr/src/Apr.cpp | 32 +++++- core/libs/tests/GuestAmpr.cpp | 97 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 192 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index f5bb93e89..bfb04045c 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -46,6 +46,29 @@ static bool ValidWriteCounter_04_00(std::uint32_t counter, std::uint32_t access, static constexpr std::uint64_t MeasureInvalid = static_cast(SCE_KERNEL_ERROR_EINVAL); +static bool ValidReadLength(std::uint64_t length) { + return length != 0u && length <= 0x100000000ull; +} + +static bool ValidReadOffset(std::uint64_t offset) { + return offset < 0x10000000000ull; +} + +static bool ValidRead(const void* destination, std::uint64_t length, std::uint64_t offset) { + constexpr std::uint64_t userLimit = 0xF00000000000ull; + const auto address = reinterpret_cast(destination); + return ValidReadLength(length) && address <= userLimit && length <= userLimit - address && ValidReadOffset(offset); +} + +static int AppendRead(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, std::uint32_t fileId, const void* destination, std::uint64_t size, std::uint64_t offset) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (opcode != Apr::Opcode::ReadFile && (buffer->recording & Apr::ScatterGatherValid) == 0u) return SCE_KERNEL_ERROR_EINVAL; + Apr::ReadFileCommand command{{opcode, sizeof(command)}, fileId, 0, reinterpret_cast(destination), size, offset}; + const int result = Append(buffer, &command, sizeof(command)); + if (result == 0) buffer->recording |= Apr::ScatterGatherValid; + return result; +} + static std::uint64_t NopBytes(std::uint32_t dwords) { return sizeof(Apr::CommandHeader) + std::uint64_t{dwords} * 4u; } @@ -167,33 +190,35 @@ int APS5_VABI sceAmprAprCommandBufferMapEnd() { int APS5_VABI sceAmprAprCommandBufferReadFile(Apr::CommandBufferObject* buffer, uint64_t* gatherState, uint64_t* scatterState, uint32_t fileId, void* destination, uint64_t size, uint64_t offset) { (void)gatherState; (void)scatterState; - Apr::ReadFileCommand command{}; - command.header = {Apr::Opcode::ReadFile, sizeof(command)}; - command.fileId = fileId; - command.destination = reinterpret_cast(destination); - command.size = size; - command.offset = offset; - return Append(buffer, &command, sizeof(command)); + if (!ValidRead(destination, size, offset)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFile, fileId, destination, size, offset); } -int APS5_VABI sceAmprAprCommandBufferReadFileGather() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, std::uint64_t size, std::uint64_t offset) { + (void)mapState; + (void)scatterGatherState; + if (!ValidReadLength(size) || !ValidReadOffset(offset)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFileGather, 0, nullptr, size, offset); } -int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, void* destination, std::uint64_t size, std::uint64_t offset) { + (void)mapState; + (void)scatterGatherState; + if (!ValidRead(destination, size, offset)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFileGatherScatter, 0, destination, size, offset); } -int APS5_VABI sceAmprAprCommandBufferReadFileScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferReadFileScatter(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, void* destination, std::uint64_t size) { + (void)mapState; + (void)scatterGatherState; + if (!ValidRead(destination, size, 0)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFileScatter, 0, destination, size, 0); } -int APS5_VABI sceAmprAprCommandBufferResetGatherScatterState() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferResetGatherScatterState(Apr::CommandBufferObject* buffer) { + const int result = AppendCommand(buffer, Apr::Opcode::ResetGatherScatterState, Apr::ResetGatherScatterStateCommand{}); + if (result == 0) buffer->recording &= static_cast(~Apr::ScatterGatherValid); + return result; } int APS5_VABI sceAmprCommandBufferClearBuffer(Apr::CommandBufferObject* buffer) { @@ -397,24 +422,23 @@ uint32_t APS5_VABI sceAmprMeasureCommandSizeReadFile(void) { return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeReadFileGather() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGather(std::uint64_t size, std::uint64_t offset) { + if (!ValidReadLength(size) || !ValidReadOffset(offset)) return MeasureInvalid; + return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void* destination, std::uint64_t size, std::uint64_t offset) { + if (!ValidRead(destination, size, offset)) return MeasureInvalid; + return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeReadFileScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileScatter(void* destination, std::uint64_t size) { + if (!ValidRead(destination, size, 0)) return MeasureInvalid; + return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState() { + return sizeof(Apr::ResetGatherScatterStateCommand); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char* text) { diff --git a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index 1ac5932a8..99bd8beb7 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -8,7 +8,7 @@ // The guest treats both the command buffer object and its memory as opaque, so the encoding is ours. namespace Apr { -enum class BufferType : std::uint32_t { +enum class BufferType : std::uint16_t { Generic = 0, Apr = 1, }; @@ -19,9 +19,12 @@ struct CommandBufferObject { std::uint32_t offset; std::uint32_t numCommands; BufferType type; + std::uint16_t recording; }; static_assert(sizeof(CommandBufferObject) == 0x18, "guest reserves 0x18 bytes for sce::Ampr::CommandBuffer"); +inline constexpr std::uint16_t ScatterGatherValid = 1; + enum class Opcode : std::uint32_t { Nop = 0, ReadFile = 1, @@ -36,6 +39,10 @@ enum class Opcode : std::uint32_t { PushMarker = 10, PopMarker = 11, SetMarker = 12, + ReadFileGather = 13, + ReadFileScatter = 14, + ReadFileGatherScatter = 15, + ResetGatherScatterState = 16, }; struct CommandHeader { @@ -52,6 +59,10 @@ struct ReadFileCommand { std::uint64_t offset; }; +struct ResetGatherScatterStateCommand { + CommandHeader header; +}; + struct WriteAddressCommand { CommandHeader header; std::uint64_t address; diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 79ab4fde2..7c652701b 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -239,8 +239,27 @@ TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { return command; } +struct ReadCursor { + bool valid = false; + std::uint32_t fileId = 0; + std::uint64_t nextDestination = 0; + std::uint64_t nextOffset = 0; +}; + +void _readResolved(Apr::Opcode opcode, Apr::ReadFileCommand command, ReadCursor& read) { + if (opcode != Apr::Opcode::ReadFile) { + if (!read.valid) throw std::runtime_error("APR: gather or scatter read without a preceding read"); + command.fileId = read.fileId; + if (opcode == Apr::Opcode::ReadFileGather) command.destination = read.nextDestination; + if (opcode == Apr::Opcode::ReadFileScatter) command.offset = read.nextOffset; + } + _readFile(command); + read = {true, command.fileId, command.destination + command.size, command.offset + command.size}; +} + void _execute(const Apr::CommandBufferObject& buffer) { std::uint32_t cursor = 0; + ReadCursor read; for (std::uint32_t index = 0; index < buffer.numCommands; ++index) { if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; @@ -252,12 +271,15 @@ void _execute(const Apr::CommandBufferObject& buffer) { case Apr::Opcode::PopMarker: case Apr::Opcode::SetMarker: break; - case Apr::Opcode::ReadFile: { - Apr::ReadFileCommand command; - std::memcpy(&command, buffer.base + cursor, sizeof(command)); - _readFile(command); + case Apr::Opcode::ReadFile: + case Apr::Opcode::ReadFileGather: + case Apr::Opcode::ReadFileScatter: + case Apr::Opcode::ReadFileGatherScatter: + _readResolved(header.opcode, _read(buffer, cursor), read); + break; + case Apr::Opcode::ResetGatherScatterState: + read = {}; break; - } case Apr::Opcode::WriteAddress: { Apr::WriteAddressCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index b2ff45a58..23106e798 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -53,6 +55,16 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00(std::uint8_ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00(std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject*, std::int16_t, const void*, std::uint32_t, const std::uint32_t*); int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject*, std::uint32_t, const char*, const std::uint32_t*); +int APS5_VABI sceKernelAprResolveFilepathsToIds(const char**, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceAmprAprCommandBufferReadFile(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint32_t, void*, std::uint64_t, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint64_t, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferReadFileScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferResetGatherScatterState(Apr::CommandBufferObject*); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGather(std::uint64_t, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileScatter(void*, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void*, std::uint64_t, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState(); } static void Require(bool value) { if (!value) std::abort(); } @@ -400,6 +412,90 @@ void TestConstructed() { Require(empty.Offset() == 0 && empty.Commands() == 0); } +std::uint8_t FileByte(std::size_t offset) { + return static_cast(offset * 7u + 3u); +} + +bool MatchesFile(const std::uint8_t* data, std::size_t offset, std::size_t bytes) { + for (std::size_t index = 0; index < bytes; ++index) { + if (data[index] != FileByte(offset + index)) return false; + } + return true; +} + +void TestGatherScatter() { + const char* path = "ampr_gather_scatter.bin"; + { + std::ofstream file(path, std::ios::binary); + for (std::size_t offset = 0; offset < 4096; ++offset) file.put(static_cast(FileByte(offset))); + } + std::uint32_t fileId = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + + Recorder recorder; + auto* buffer = &recorder.buffer; + auto* map = &recorder.gatherState; + auto* state = &recorder.scatterState; + std::array first{}; + std::array second{}; + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == invalidArgument); + Require(sceAmprAprCommandBufferReadFileGatherScatter(buffer, map, state, second.data(), 8, 0) == invalidArgument); + Require(recorder.Commands() == 0); + + Require(sceAmprAprCommandBufferReadFile(buffer, map, state, fileId, first.data(), 16, 100) == 0); + auto offset = recorder.Offset(); + auto commands = recorder.Commands(); + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 500) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileGather, sceAmprMeasureCommandSizeReadFileGather(8, 500)); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileScatter, sceAmprMeasureCommandSizeReadFileScatter(second.data(), 8)); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprAprCommandBufferReadFileGatherScatter(buffer, map, state, second.data() + 32, 4, 1000) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileGatherScatter, sceAmprMeasureCommandSizeReadFileGatherScatter(second.data() + 32, 4, 1000)); + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 4, 2000) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, first.data() + 40, 6) == 0); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprAprCommandBufferResetGatherScatterState(buffer) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ResetGatherScatterState, sceAmprMeasureCommandSizeResetGatherScatterState()); + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(buffer, map, state, fileId, second.data() + 48, 4, 3000) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data() + 56, 4) == 0); + Require(recorder.Commands() == 9); + SubmitWithin10Seconds(recorder); + + Require(MatchesFile(first.data(), 100, 16) && MatchesFile(first.data() + 16, 500, 8)); + Require(MatchesFile(second.data(), 508, 8)); + Require(MatchesFile(second.data() + 32, 1000, 4) && MatchesFile(second.data() + 36, 2000, 4)); + Require(MatchesFile(first.data() + 40, 2004, 6)); + Require(MatchesFile(second.data() + 48, 3000, 4) && MatchesFile(second.data() + 56, 3004, 4)); + Require(first[24] == 0 && first[39] == 0 && second[8] == 0 && second[31] == 0 && second[40] == 0); + std::remove(path); + + const auto rejected = static_cast(static_cast(invalidArgument)); + auto* high = reinterpret_cast(std::uintptr_t{0xF00000000000ull}); + Recorder empty; + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, first.data(), 0, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, first.data(), 0x100000001ull, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, first.data(), 4, 0x10000000000ull) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, high, 4, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(nullptr, map, state, fileId, first.data(), 4, 0) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); + Require(sceAmprMeasureCommandSizeReadFileGather(0, 0) == rejected); + Require(sceAmprMeasureCommandSizeReadFileGather(0x100000000ull, 0x10000000000ull - 1u) == sizeof(Apr::ReadFileCommand)); + Require(sceAmprMeasureCommandSizeReadFileGather(4, 0x10000000000ull) == rejected); + Require(sceAmprMeasureCommandSizeReadFileScatter(first.data(), 0x100000001ull) == rejected); + Require(sceAmprMeasureCommandSizeReadFileScatter(high, 4) == rejected); + Require(sceAmprMeasureCommandSizeReadFileGatherScatter(first.data(), 4, 0x10000000000ull) == rejected); + Require(sceAmprMeasureCommandSizeReadFileGatherScatter(nullptr, 4, 0) == sizeof(Apr::ReadFileCommand)); +} + } int main() { @@ -418,5 +514,6 @@ int main() { TestVersionedCommands(); TestVersionedCounters(); TestConstructed(); + TestGatherScatter(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5615df0f0..cc45f9093 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -34,6 +34,7 @@ Throughout the project, every function at every stage either **does exactly what - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR counter access](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded +- [APR gather and scatter reads](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` From 41fc6c631c1f96a66e1c7fcd208886be7a98b4fe Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:06:04 +0200 Subject: [PATCH 23/43] feat(shader): implement s_swappc_b64 null, long branches and dword jump tables SOP1 opcode 33 with SDST null keeps no return address, so it is exactly s_setpc_b64; the decoder now reads it that way, as KytyPS5 does. A call with a real destination still throws "unsupported SOP1 opcode 33". GraphBuilder resolves two more jump shapes. A long branch is s_getpc_b64 and s_add_u32/s_addc_u32 or s_sub_u32/s_subb_u32 immediates, also through vcc, all in one block. A dword jump table branches to the table base minus an entry that s_load_dword reads at an index bounded by s_min_u32 and scaled by 4; the resolver follows register writes back through straight-line code, and anything else is still refused. The code table load now also handles one-dword entries. agc_shader_control_flow checks the decode, the graphs and the refusals; the new agc_driver_scalar_swappc runs all shapes on the GPU. --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 167 ++++++++++++++++++ .../tests/execution/ScalarSwappc.cpp | 143 +++++++++++++++ .../ControlFlow/src/GraphBuilder.cpp | 166 ++++++++++++++++- .../RdnaDecoder/src/RdnaScalarOpDecoder.cpp | 2 +- .../ScalarBufferInstructions.cpp | 8 +- 5 files changed, 480 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappc.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index d7fac7e84..aea72c426 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -208,6 +209,80 @@ void verifyProgram(const Program& program) { } } +const std::vector DwordTableGetpcFirst{ + 0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u, + 0xbf8cc07fu, 0x7d820003u, 0x808c0e0cu, 0x828d800du, 0xbefd210cu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, + 0xe0700000u, 0x80000100u, 0xbf810000u, 0x00000020u, 0x00000018u, 0x00000010u}; +const std::vector DwordTableIndexFirst{ + 0x816ac102u, 0x83ea826au, 0x7e020280u, 0x8f6a826au, 0xbe8e1f00u, 0x800ebc0eu, 0x820f800fu, 0xf4000487u, 0xd4000008u, 0x8aea167eu, + 0xbf8cc07fu, 0x808e120eu, 0x828f800fu, 0xbefd210eu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x00000018u, 0x00000010u, 0x00000008u}; +const std::vector LongBranchVcc{ + 0x7e020281u, 0xbeea1f00u, 0x806a906au, 0x826b806bu, 0xbefd216au, 0x7e020282u, 0xe0700000u, 0x80000100u, 0xbf810000u}; +const std::vector LongBranchBack{ + 0xbe880380u, 0x7e020280u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf840004u, 0xbe901f00u, 0x80909410u, 0x82918011u, 0xbefd2110u, + 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector patched(std::vector code, std::size_t word, std::initializer_list replacement) { + std::copy(replacement.begin(), replacement.end(), code.begin() + static_cast(word)); + return code; +} + +std::string refusal(std::span code) { + try { + static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code))); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +void verifyJumpTable(const std::string& name, std::span code, std::uint32_t loadPc, const std::vector& values, const std::vector& targets) { + const auto graph = GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code)); + require(graph.codeTableLoads.size() == 1u && graph.codeTableLoads.front().programCounter == loadPc && graph.codeTableLoads.front().values == values, name + ": wrong code table"); + std::vector lowered; + for (const auto& block : graph.blocks) { + if (block.terminator.indirectPcSgpr != InvalidControlFlowId) lowered.insert(lowered.end(), block.terminator.indirectTargetProgramCounters.begin(), block.terminator.indirectTargetProgramCounters.end()); + } + std::sort(lowered.begin(), lowered.end()); + require(lowered == targets, name + ": the jump does not reach exactly the table targets"); +} + +void verifyLongBranch(const std::string& name, std::span code, std::uint32_t branchPc, std::uint32_t target) { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto graph = GraphBuilder{}.Build(decoded); + const auto source = std::find_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) { return block.instructionEnd != block.instructionBegin && decoded.instructions[block.instructionEnd - 1u].programCounter == branchPc; }); + require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target)); +} + +void verifyNullSwappc() { + const std::array jump{0xbefd210cu, 0xbf810000u}; + const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front(); + require(swappc.op == RdnaOpcode::SSetpcB64 && swappc.opcodeId == 0x21u && swappc.destination.kind == RdnaOperandKind::Null && + swappc.source0.kind == RdnaOperandKind::ScalarRegister && swappc.source0.reg == 12u, "s_swappc_b64 null, s[12:13] does not decode as s_setpc_b64 s[12:13]"); + const std::array front{0x7e020281u, 0xbefd2106u, 0xbf810000u}; + require(DecodeRdnaFrontProgram(front).code.size() == 2u, "a front program ending in s_swappc_b64 null, s[6:7] does not end there"); + verifyJumpTable("dword jump table after its base", DwordTableGetpcFirst, 0x20u, {0x20u, 0x18u, 0x10u}, {0x3cu, 0x44u, 0x4cu}); + verifyJumpTable("dword jump table after its index", DwordTableIndexFirst, 0x1cu, {0x18u, 0x10u, 0x08u}, {0x38u, 0x40u, 0x48u}); + verifyLongBranch("long branch through vcc", LongBranchVcc, 0x10u, 0x18u); + verifyLongBranch("backward long branch", LongBranchBack, 0x24u, 0x08u); + const std::array call{0xbe8c1f00u, 0xbe8e210cu, 0xbf810000u}; + const std::vector, std::string_view>> refused{ + {"s_swappc_b64 with a return address", {call.begin(), call.end()}, "unsupported SOP1 opcode 33"}, + {"index rewritten after the bound", patched(DwordTableGetpcFirst, 5, {0xbe9203ffu, 0x00000040u}), "unsupported dynamic s_setpc_b64"}, + {"index scaled for two-dword entries", patched(DwordTableGetpcFirst, 7, {0x8f128312u}), "unsupported dynamic s_setpc_b64"}, + {"entry rewritten after the load", patched(DwordTableGetpcFirst, 11, {0xbe8e0380u}), "unsupported dynamic s_setpc_b64"}, + {"base rewritten after the load", patched(DwordTableGetpcFirst, 11, {0xbe8d0380u}), "unsupported dynamic s_setpc_b64"}, + {"borrow other than zero", patched(DwordTableGetpcFirst, 13, {0x828d810du}), "unsupported dynamic s_setpc_b64"}, + {"long branch with a register high half", patched(LongBranchVcc, 3, {0x826b046bu}), "unsupported dynamic s_setpc_b64"}, + {"branch into a long branch", patched(LongBranchVcc, 0, {0xbf850001u}), "does not start in its block"}, + }; + for (const auto& [name, code, message] : refused) { + const auto error = refusal(code); + require(error.find(message) != std::string::npos, std::string(name) + ": expected a refusal with '" + std::string(message) + "', got '" + error + "'"); + } +} + } int main(int argc, char** argv) { @@ -656,6 +731,92 @@ test: {0xbe880380u, 0x7e020280u, 0xbf06c002u, 0xbf840006u, 0x4a020081u, 0xe0700000u, 0x80000100u, 0xbf810000u, 0x4a020282u, 0x80089008u, 0xbf0ac008u, 0xbf85fffcu, 0xe0700000u, 0x80000100u, 0xbf810000u}, Split::None}, + {"dword jump table after its base", R"( + s_getpc_b64 s[12:13] +base: + s_add_u32 s12, s12, table - base + s_addc_u32 s13, s13, 0 + s_min_u32 s18, s2, 2 + v_cmp_gt_u32 s[22:23], v0, s2 + s_lshl_b32 s18, s18, 2 + s_load_dword s14, s[12:13], s18 + s_waitcnt lgkmcnt(0) + v_cmp_lt_u32 vcc, s3, v0 + s_sub_u32 s12, s12, s14 + s_subb_u32 s13, s13, 0 + s_swappc_b64 null, s[12:13] +case0: + v_mov_b32 v1, 1 + s_branch done +case1: + v_mov_b32 v1, 2 + s_branch done +case2: + v_mov_b32 v1, 3 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm +table: + .long table - case0, table - case1, table - case2)", + DwordTableGetpcFirst, Split::None}, + {"dword jump table after its index", R"( + s_add_i32 vcc_lo, s2, -1 + s_min_u32 vcc_lo, vcc_lo, 2 + v_mov_b32 v1, 0 + s_lshl_b32 vcc_lo, vcc_lo, 2 + s_getpc_b64 s[14:15] +base: + s_add_u32 s14, s14, table - base - 8 + s_addc_u32 s15, s15, 0 + s_load_dword s18, s[14:15], vcc_lo offset:8 + s_andn2_b64 vcc, exec, s[22:23] + s_waitcnt lgkmcnt(0) + s_sub_u32 s14, s14, s18 + s_subb_u32 s15, s15, 0 + s_swappc_b64 null, s[14:15] +case0: + v_mov_b32 v1, 1 + s_branch done +case1: + v_mov_b32 v1, 2 + s_branch done +case2: + v_mov_b32 v1, 3 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm +table: + .long table - 8 - case0, table - 8 - case1, table - 8 - case2)", + DwordTableIndexFirst, Split::None}, + {"long branch through vcc", R"( + v_mov_b32 v1, 1 + s_getpc_b64 vcc +base: + s_add_u32 vcc_lo, vcc_lo, target - base + s_addc_u32 vcc_hi, vcc_hi, 0 + s_swappc_b64 null, vcc + v_mov_b32 v1, 2 +target: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + LongBranchVcc, Split::None}, + {"backward long branch", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 +loop: + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc0 done + s_getpc_b64 s[16:17] +base: + s_sub_u32 s16, s16, base - loop + s_subb_u32 s17, s17, 0 + s_swappc_b64 null, s[16:17] +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + LongBranchBack, Split::None}, }; int failures = 0; for (const auto& program : programs) { @@ -666,6 +827,12 @@ test: ++failures; } } + try { + verifyNullSwappc(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + ++failures; + } if (failures != 0) return 1; std::puts("Control flow structurization tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappc.cpp new file mode 100644 index 000000000..161d76cea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappc.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Wave32Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0xdeadbeefu, 0x7e1602ffu, 0xdeadbeefu, + 0x7e1802ffu, 0xdeadbeefu, 0x7e1a02ffu, 0xdeadbeefu, 0xbe940380u, 0xd7600008u, 0x02002904u, 0xd760000au, + 0x02002905u, 0xbe8c1f00u, 0x800cff0cu, 0x0000014cu, 0x820d800du, 0xbe89030au, 0x83928308u, 0xd4c40016u, + 0x02001104u, 0x8f128212u, 0xf4000386u, 0x24000000u, 0xbf8cc07fu, 0x7d820a0au, 0x808c0e0cu, 0x828d800du, + 0xbefd210cu, 0x8009ff0au, 0x11110000u, 0xbf820008u, 0x8009ff0au, 0x22220000u, 0xbf820005u, 0x8009ff0au, + 0x33330000u, 0xbf820002u, 0x8009ff0au, 0x44440000u, 0xd761000au, 0x02002809u, 0x816ac108u, 0x83ea836au, + 0x8f6a826au, 0xbe8e1f00u, 0x800eff0eu, 0x000000d4u, 0x820f800fu, 0xf4000487u, 0xd4000008u, 0x8aea167eu, + 0xbf8cc07fu, 0x808e120eu, 0x828f800fu, 0xbefd210eu, 0x8009ff0au, 0x0a000000u, 0xbf820008u, 0x8009ff0au, + 0x0b000000u, 0xbf820005u, 0x8009ff0au, 0x0c000000u, 0xbf820002u, 0x8009ff0au, 0x0d000000u, 0xd761000bu, + 0x02002809u, 0x8009ff0au, 0x600d0000u, 0xbeea1f00u, 0x806a946au, 0x826b806bu, 0xbefd216au, 0xbe8903ffu, + 0x00000badu, 0xd761000cu, 0x02002809u, 0xbe890380u, 0xbe950383u, 0x80090a09u, 0x80958115u, 0xbf068015u, + 0xbf850004u, 0xbe901f00u, 0x80909410u, 0x82918011u, 0xbefd2110u, 0xd761000du, 0x02002809u, 0x80148114u, + 0xbf0aa014u, 0xbf85ffabu, 0xe0781000u, 0x80010a01u, 0xbf810000u, 0x00000110u, 0x00000104u, 0x000000f8u, + 0x000000ecu, 0x000000acu, 0x000000a0u, 0x00000094u, 0x00000088u, +}; + +alignas(256) constexpr std::array Wave64Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0xdeadbeefu, 0x7e1602ffu, 0xdeadbeefu, + 0x7e1802ffu, 0xdeadbeefu, 0x7e1a02ffu, 0xdeadbeefu, 0xbe940380u, 0xd7600008u, 0x02002904u, 0xd760000au, + 0x02002905u, 0xbe8c1f00u, 0x800cff0cu, 0x0000014cu, 0x820d800du, 0xbe89030au, 0x83928308u, 0xd4c40016u, + 0x02001104u, 0x8f128212u, 0xf4000386u, 0x24000000u, 0xbf8cc07fu, 0x7d820a0au, 0x808c0e0cu, 0x828d800du, + 0xbefd210cu, 0x8009ff0au, 0x11110000u, 0xbf820008u, 0x8009ff0au, 0x22220000u, 0xbf820005u, 0x8009ff0au, + 0x33330000u, 0xbf820002u, 0x8009ff0au, 0x44440000u, 0xd761000au, 0x02002809u, 0x816ac108u, 0x83ea836au, + 0x8f6a826au, 0xbe8e1f00u, 0x800eff0eu, 0x000000d4u, 0x820f800fu, 0xf4000487u, 0xd4000008u, 0x8aea167eu, + 0xbf8cc07fu, 0x808e120eu, 0x828f800fu, 0xbefd210eu, 0x8009ff0au, 0x0a000000u, 0xbf820008u, 0x8009ff0au, + 0x0b000000u, 0xbf820005u, 0x8009ff0au, 0x0c000000u, 0xbf820002u, 0x8009ff0au, 0x0d000000u, 0xd761000bu, + 0x02002809u, 0x8009ff0au, 0x600d0000u, 0xbeea1f00u, 0x806a946au, 0x826b806bu, 0xbefd216au, 0xbe8903ffu, + 0x00000badu, 0xd761000cu, 0x02002809u, 0xbe890380u, 0xbe950383u, 0x80090a09u, 0x80958115u, 0xbf068015u, + 0xbf850004u, 0xbe901f00u, 0x80909410u, 0x82918011u, 0xbefd2110u, 0xd761000du, 0x02002809u, 0x80148114u, + 0xbf0ac014u, 0xbf85ffabu, 0xe0781000u, 0x80010a01u, 0xbf810000u, 0x00000110u, 0x00000104u, 0x000000f8u, + 0x000000ecu, 0x000000acu, 0x000000a0u, 0x00000094u, 0x00000088u, +}; + +constexpr std::array Selectors{0u, 1u, 2u, 3u, 4u, 7u, 0x80000000u, 0xffffffffu}; +constexpr std::array GetpcFirstCases{0x11110000u, 0x22220000u, 0x33330000u, 0x44440000u}; +constexpr std::array IndexFirstCases{0x0a000000u, 0x0b000000u, 0x0c000000u, 0x0d000000u}; +constexpr const char* Names[Results] = {"dword_table_getpc_first", "dword_table_index_first", "long_branch_vcc", "long_branch_back"}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Expected(std::uint32_t lane, std::uint32_t result) { + const auto selector = Input[lane * Inputs]; + const auto value = Input[lane * Inputs + 1u]; + switch (result) { + case 0: return value + GetpcFirstCases[std::min(selector, 3u)]; + case 1: return value + IndexFirstCases[std::min(selector - 1u, 3u)]; + case 2: return value + 0x600d0000u; + default: return value * 3u; + } +} + +ShaderRecompiler::RecompileResult Compile(std::span code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { + for (std::uint32_t tid = 0; tid < MaxThreads; ++tid) { + Input[tid * Inputs] = Selectors[tid % Selectors.size()]; + Input[tid * Inputs + 1u] = 0x9e3779b9u * (tid + 1u); + } + Output.fill(0xdeadbeefu); + const auto result = Compile(code, waveSize, target); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t lanes) { + for (std::uint32_t tid = 0; tid < lanes; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const auto actual = Output[tid * Results + index]; + const auto expected = Expected(tid, index); + Require(actual == expected, "swappc wave" + std::to_string(lanes) + ": lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, Wave32Code, 32, device->ComputeTarget(32)); + Check(32); + Run(*device, Wave64Code, 64, device->Target()); + Check(64); + Run(*device, Wave64Code, 64, device->ComputeTarget(32)); + Check(64); + std::puts("swappc tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 2c5ae693d..cfcb917ff 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -110,10 +111,59 @@ bool isImmediate(const RdnaOperand& operand, std::uint32_t& value) { return false; } +constexpr std::uint32_t NoScalarRegister = UINT32_MAX; + +std::uint32_t scalarIndex(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::ScalarRegister: return operand.reg; + case RdnaOperandKind::VccLo: return 106u; + case RdnaOperandKind::VccHi: return 107u; + default: return NoScalarRegister; + } +} + +bool isScalar(const RdnaOperand& operand, std::uint32_t index) { + return index != NoScalarRegister && scalarIndex(operand) == index; +} + +bool addsImmediateTo(const RdnaInstruction& instruction, std::uint32_t index, std::uint32_t& immediate) { + return isScalar(instruction.destination, index) && + ((isScalar(instruction.source0, index) && isImmediate(instruction.source1, immediate)) || + (isScalar(instruction.source1, index) && isImmediate(instruction.source0, immediate))); +} + +bool subtractsImmediateFrom(const RdnaInstruction& instruction, std::uint32_t index, std::uint32_t& immediate) { + return isScalar(instruction.destination, index) && isScalar(instruction.source0, index) && isImmediate(instruction.source1, immediate); +} + +bool resolveLongSetpcTarget(const RdnaProgram& program, std::uint32_t setpcIndex, std::uint32_t& target) { + if (setpcIndex < 3u) return false; + const auto& setpc = program.instructions[setpcIndex]; + const auto& pc = program.instructions[setpcIndex - 3u]; + const auto& low = program.instructions[setpcIndex - 2u]; + const auto& high = program.instructions[setpcIndex - 1u]; + const auto pcRegister = scalarIndex(setpc.source0); + const bool adds = low.op == RdnaOpcode::SAddU32 && high.op == RdnaOpcode::SAddcU32; + const bool subtracts = low.op == RdnaOpcode::SSubU32 && high.op == RdnaOpcode::SSubbU32; + std::uint32_t lowImmediate = 0, highImmediate = 0; + if (setpc.op != RdnaOpcode::SSetpcB64 || pcRegister == NoScalarRegister || pcRegister % 2u != 0u || pc.op != RdnaOpcode::SGetpcB64 || !isScalar(pc.destination, pcRegister)) return false; + if (adds ? !addsImmediateTo(low, pcRegister, lowImmediate) || !addsImmediateTo(high, pcRegister + 1u, highImmediate) + : !subtracts || !subtractsImmediateFrom(low, pcRegister, lowImmediate) || !subtractsImmediateFrom(high, pcRegister + 1u, highImmediate)) return false; + const auto offset = (static_cast(highImmediate) << 32u) | lowImmediate; + const auto base = static_cast(instructionEndProgramCounter(pc)); + const auto destination = adds ? base + offset : base - offset; + if (destination > UINT32_MAX || (destination & 3u) != 0u) return false; + target = static_cast(destination); + return true; +} + bool resolveSetpcTarget(const RdnaProgram& program, std::uint32_t setpcIndex, std::uint32_t& target) { if (setpcIndex >= program.instructions.size()) { return false; } + if (resolveLongSetpcTarget(program, setpcIndex, target)) { + return true; + } const auto& setpc = program.instructions[setpcIndex]; if (setpc.op != RdnaOpcode::SSetpcB64 || setpc.source0.kind != RdnaOperandKind::ScalarRegister) { @@ -233,6 +283,115 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu return false; } +bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { + std::uint32_t count = instruction.dataDwordCount; + switch (instruction.family) { + case RdnaInstructionFamily::SOP1: + case RdnaInstructionFamily::SOP2: + case RdnaInstructionFamily::SOPK: + if (instruction.op == RdnaOpcode::SMovreldB32 || instruction.op == RdnaOpcode::SMovreldB64 || instruction.op == RdnaOpcode::SMovrelsd2B32) return true; + break; + case RdnaInstructionFamily::SMEM: + if (instruction.op == RdnaOpcode::SMemtime || instruction.op == RdnaOpcode::SMemrealtime) count = 2u; + break; + case RdnaInstructionFamily::VOP1: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: + count = 2u; + break; + case RdnaInstructionFamily::SOPC: + case RdnaInstructionFamily::SOPP: + case RdnaInstructionFamily::VINTRP: + case RdnaInstructionFamily::MUBUF: + case RdnaInstructionFamily::MTBUF: + case RdnaInstructionFamily::FLAT: + case RdnaInstructionFamily::DS: + case RdnaInstructionFamily::MIMG: + case RdnaInstructionFamily::EXP: return false; + case RdnaInstructionFamily::Unknown: return true; + } + const auto covers = [&](const RdnaOperand& operand) { + const auto first = scalarIndex(operand); + return first != NoScalarRegister && index >= first && index - first < count; + }; + return covers(instruction.destination) || covers(instruction.destination2); +} + +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer) { + for (auto position = end; position-- > 0u;) { + const auto& instruction = program.instructions[position]; + if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; + if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index); })) { + writer = position; + return true; + } + } + return false; +} + +bool resolveDwordJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { + if (index < 2u) return false; + const auto& branch = program.instructions[index]; + const auto& low = program.instructions[index - 2u]; + const auto& high = program.instructions[index - 1u]; + if (branch.source0.kind != RdnaOperandKind::ScalarRegister || branch.source0.reg % 2u != 0u) return false; + const auto pcReg = branch.source0.reg; + const auto entryReg = scalarIndex(low.source1); + std::uint32_t borrow = 0; + if (low.op != RdnaOpcode::SSubU32 || !isScalar(low.destination, pcReg) || !isScalar(low.source0, pcReg) || + entryReg == NoScalarRegister || entryReg == pcReg || entryReg == pcReg + 1u || + high.op != RdnaOpcode::SSubbU32 || !subtractsImmediateFrom(high, pcReg + 1u, borrow) || borrow != 0u) return false; + std::uint32_t loadIndex = 0, shiftIndex = 0, boundIndex = 0, baseIndex = 0; + if (!findLastWriter(program, index - 2u, {entryReg, pcReg, pcReg + 1u}, loadIndex)) return false; + const auto& load = program.instructions[loadIndex]; + const auto offsetReg = scalarIndex(load.source1); + if (load.op != RdnaOpcode::SLoadDword || !isScalar(load.destination, entryReg) || writesScalar(load, pcReg) || writesScalar(load, pcReg + 1u) || + !isScalar(load.source0, pcReg) || offsetReg == NoScalarRegister || offsetReg == pcReg || offsetReg == pcReg + 1u) return false; + if (!findLastWriter(program, loadIndex, {offsetReg}, shiftIndex) || !findLastWriter(program, shiftIndex, {offsetReg}, boundIndex) || + !findLastWriter(program, loadIndex, {pcReg, pcReg + 1u}, baseIndex) || baseIndex < 2u) return false; + const auto& shift = program.instructions[shiftIndex]; + const auto& bound = program.instructions[boundIndex]; + const auto& basePc = program.instructions[baseIndex - 2u]; + const auto& baseLow = program.instructions[baseIndex - 1u]; + const auto& baseHigh = program.instructions[baseIndex]; + std::uint32_t shiftAmount = 0, maximum = 0, displacement = 0, carry = 0; + if (shift.op != RdnaOpcode::SLshlB32 || !isScalar(shift.destination, offsetReg) || !isScalar(shift.source0, offsetReg) || + !isImmediate(shift.source1, shiftAmount) || shiftAmount != 2u || + bound.op != RdnaOpcode::SMinU32 || !isScalar(bound.destination, offsetReg) || + !(isImmediate(bound.source1, maximum) || isImmediate(bound.source0, maximum)) || maximum > 255u || + basePc.op != RdnaOpcode::SGetpcB64 || !isScalar(basePc.destination, pcReg) || + baseLow.op != RdnaOpcode::SAddU32 || !addsImmediateTo(baseLow, pcReg, displacement) || + baseHigh.op != RdnaOpcode::SAddcU32 || !addsImmediateTo(baseHigh, pcReg + 1u, carry) || carry != 0u) return false; + const auto& first = program.instructions[std::min(boundIndex, baseIndex - 2u)]; + for (const auto& instruction : program.instructions) { + if (IsDirectBranchOpcode(instruction.op) && instruction.branchTarget > first.programCounter && instruction.branchTarget <= branch.programCounter) return false; + } + const auto base = static_cast(instructionEndProgramCounter(basePc)) + displacement; + const auto table = base + static_cast(load.memoryOffset); + const auto byteCount = (static_cast(maximum) + 1) * 4; + if (table < 0 || (table & 3) != 0 || table + byteCount > static_cast(program.code.size_bytes())) return false; + BoundedJumpTable resolved; + resolved.load.programCounter = load.programCounter; + resolved.firstProgramCounter = first.programCounter; + for (std::uint32_t entry = 0; entry <= maximum; ++entry) { + const auto value = program.code[static_cast(table / 4) + entry]; + const auto target = base - static_cast(value); + if (target < static_cast(instructionEndProgramCounter(branch)) || target > UINT32_MAX || (target & 3) != 0) return false; + resolved.load.values.push_back(value); + resolved.targets.push_back(static_cast(target)); + } + result = std::move(resolved); + return true; +} + +bool resolveBoundedJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { + if (resolveJumpTable(program, index, result)) return true; + result = BoundedJumpTable{}; + return resolveDwordJumpTable(program, index, result); +} + BranchCondition conditionForOpcode(RdnaOpcode opcode) { switch (opcode) { case RdnaOpcode::SBranch: return BranchCondition::Always; @@ -346,7 +505,7 @@ std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program std::uint32_t target = 0; if (!resolveSetpcTarget(program, index, target)) { BoundedJumpTable table; - if (!resolveJumpTable(program, index, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(instruction.programCounter)); + if (!resolveBoundedJumpTable(program, index, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(instruction.programCounter)); for (const auto tableTarget : table.targets) { if (!instructionProgramCounters.contains(tableTarget)) throw std::invalid_argument("jump table targets invalid instruction boundary " + toHexString(tableTarget)); labels.insert(tableTarget); @@ -413,7 +572,7 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram std::uint32_t target = 0; if (!resolveSetpcTarget(program, block.instructionEnd - 1u, target)) { BoundedJumpTable table; - if (!resolveJumpTable(program, block.instructionEnd - 1u, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(last.programCounter)); + if (!resolveBoundedJumpTable(program, block.instructionEnd - 1u, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(last.programCounter)); block.terminator.kind = TerminatorKind::IndirectBranch; block.terminator.indirectPcSgpr = last.source0.reg; sortUnique(table.targets); @@ -424,6 +583,7 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram } continue; } + if (block.instructionEnd - block.instructionBegin < 4u && resolveLongSetpcTarget(program, block.instructionEnd - 1u, target)) throw std::invalid_argument("long branch at program counter " + toHexString(last.programCounter) + " does not start in its block"); block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; block.terminator.trueBlock = programCounterToBlock.at(target); @@ -485,7 +645,7 @@ ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program) const { const auto term = graph.blocks[id].terminator; if (term.kind != TerminatorKind::IndirectBranch) continue; BoundedJumpTable table; - if (!resolveJumpTable(program, graph.blocks[id].instructionEnd - 1u, table)) throw std::logic_error("jump table resolution changed"); + if (!resolveBoundedJumpTable(program, graph.blocks[id].instructionEnd - 1u, table)) throw std::logic_error("jump table resolution changed"); if (table.firstProgramCounter < graph.blocks[id].startProgramCounter) throw std::invalid_argument("jump table bound does not dominate its branch in the same block"); graph.codeTableLoadProgramCounters.push_back(table.load.programCounter); graph.codeTableLoads.push_back(std::move(table.load)); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index 09ef291c1..de3c85f62 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -328,7 +328,7 @@ RdnaInstruction DecodeRdnaSop1(std::uint32_t programCounter, std::span(table.values.back()))); IrU32 high(ir.Constant(static_cast(table.values.back() >> 32u))); for (std::size_t entry = table.values.size() - 1u; entry-- > 0u;) { - auto& matches = ir.IEqual(index.Value(), ir.Constant(static_cast(entry * 8u))); + auto& matches = ir.IEqual(index.Value(), ir.Constant(static_cast(entry * instruction.dataDwordCount * 4u))); low = IrU32(ir.Select(matches, ir.Constant(static_cast(table.values[entry])), low.Value())); - high = IrU32(ir.Select(matches, ir.Constant(static_cast(table.values[entry] >> 32u)), high.Value())); + if (instruction.dataDwordCount == 2u) high = IrU32(ir.Select(matches, ir.Constant(static_cast(table.values[entry] >> 32u)), high.Value())); + } + if (instruction.dataDwordCount == 1u) { + writeRawU32(instruction.destination, low); + return; } writeU32Pair(instruction.destination, {low, high}); } From 9489b851ae260a1836c289bc4662813e1a504104 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 22:48:06 +0200 Subject: [PATCH 24/43] fix(libkernel): read defaultLanguage from localizedParameters A PS5 param.json keeps defaultLanguage inside localizedParameters, next to the language entries. The parser looked for it at the root only, then fell back to en-US and to the first member, which is the defaultLanguage string when no en-US entry exists (PPSA12544 has only en-GB), and threw "expected JSON object". defaultLanguage is now read from localizedParameters first, and the fallback takes the first language object. Co-Authored-By: Claude Opus 5.5 --- .../AppMetadata/src/ParamJsonParser.cpp | 13 ++++++-- core/libs/tests/ParamJsonParser.cpp | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index aa36abe72..a584cf3b2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -249,12 +249,21 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { if (localizedParametersValue->type != JsonType::Object) throw std::runtime_error("param.json localizedParameters is not an object"); if (localizedParametersValue->objectValue.empty()) throw std::runtime_error("param.json localizedParameters is empty"); std::string selectedLanguage; - const JsonValue* defaultLanguageValue = findObjectMember(root, "defaultLanguage"); + const JsonValue* defaultLanguageValue = findObjectMember(*localizedParametersValue, "defaultLanguage"); + if (defaultLanguageValue == nullptr) defaultLanguageValue = findObjectMember(root, "defaultLanguage"); if (defaultLanguageValue != nullptr) selectedLanguage = asString(*defaultLanguageValue, "defaultLanguage"); const JsonValue* languageObject = nullptr; if (!selectedLanguage.empty()) languageObject = findObjectMember(*localizedParametersValue, selectedLanguage); if (languageObject == nullptr) languageObject = findObjectMember(*localizedParametersValue, "en-US"); - if (languageObject == nullptr) languageObject = &localizedParametersValue->objectValue.front().second; + if (languageObject == nullptr) { + for (const auto& member : localizedParametersValue->objectValue) { + if (member.second.type == JsonType::Object) { + languageObject = &member.second; + break; + } + } + } + if (languageObject == nullptr) throw std::runtime_error("param.json localizedParameters has no language entry"); const JsonValue* titleValue = findObjectMember(*languageObject, "titleName"); if (titleValue == nullptr) throw std::runtime_error("param.json localized entry is missing titleName"); const std::string title = asString(*titleValue, "titleName"); diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index fc3c06cb0..88684685d 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -24,6 +24,37 @@ static ParsedParamJson ParseSize(const std::filesystem::path& path, const std::s return parseParamJson(path); } +static ParsedParamJson ParseText(const std::filesystem::path& path, const std::string& text) { + { + std::ofstream file(path, std::ios::binary); + file << text; + Require(static_cast(file), "Cannot write param.json fixture"); + } + return parseParamJson(path); +} + +static void CheckLanguages(const std::filesystem::path& path) { + const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" + " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); + Require(nested.title == "British" && nested.titleId == "PPSA00001", "Nested defaultLanguage without en-US"); + const auto selected = ParseText(path, R"({"titleId":"PPSA00002","localizedParameters":{"defaultLanguage":"fr-FR",)" + R"("en-US":{"titleName":"American"},"fr-FR":{"titleName":"French"}}})"); + Require(selected.title == "French", "Nested defaultLanguage not selected"); + const auto american = ParseText(path, R"({"titleId":"PPSA00003","localizedParameters":{"defaultLanguage":"de-DE",)" + R"("ja-JP":{"titleName":"Japanese"},"en-US":{"titleName":"American"}}})"); + Require(american.title == "American", "Missing defaultLanguage entry does not fall back to en-US"); + const auto root = ParseText(path, R"({"titleId":"PPSA00004","defaultLanguage":"ja-JP","localizedParameters":{)" + R"("en-US":{"titleName":"American"},"ja-JP":{"titleName":"Japanese"}}})"); + Require(root.title == "Japanese", "Root defaultLanguage not selected"); + bool rejected = false; + try { + ParseText(path, R"({"titleId":"PPSA00005","localizedParameters":{"defaultLanguage":"en-GB"}})"); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "Accepted localizedParameters without a language entry"); +} + int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); @@ -47,6 +78,7 @@ int main() { try { ParseSize(path, text); } catch (const std::exception&) { rejected = true; } Require(rejected, std::string("Accepted invalid download size: ") + text); } + CheckLanguages(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; From fd37c583912eab3ada5013b4c487231fe758276b Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 23:00:41 +0200 Subject: [PATCH 25/43] fix(libkernel): report the stack of adopted threads scePthreadAttrGet reported a null stack address and a zero size for an adopted thread (the main thread), and for every guest thread on Linux, where the stack range was never recorded. A garbage collector that scans thread stacks through scePthreadAttrGetstackaddr and scePthreadAttrGetstacksize (Unity IL2CPP in PPSA12544) then skipped the main thread's stack and freed objects it still referenced. The range now comes from the host thread: GetCurrentThreadStackLimits on Windows, pthread_getattr_np on Linux. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 24 +++++++++++++++++++ core/libs/tests/GuestThreadSelf.cpp | 23 ++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 3dc69cbab..23b69cf56 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -42,6 +42,27 @@ static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; static thread_local PthreadPrivate* currentThread = nullptr; static thread_local bool threadFinishing = false; + +static void SetStackFromHost(PthreadPrivate* thread) { +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + if (high <= low) throw std::runtime_error("Cannot query the host thread stack"); + thread->stackAddress = reinterpret_cast(low); + thread->stackSize = static_cast(high - low); +#else + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) != 0) throw std::runtime_error("Cannot query the host thread stack"); + void* address = nullptr; + std::size_t size = 0; + const int result = pthread_attr_getstack(&attr, &address, &size); + pthread_attr_destroy(&attr); + if (result != 0 || size == 0) throw std::runtime_error("Cannot query the host thread stack"); + thread->stackAddress = address; + thread->stackSize = size; +#endif +} #ifndef _WIN32 static thread_local std::unique_ptr adoptedThread; // pthread_exit force-unwinds through guest frames, whose personality resolves to @@ -96,6 +117,7 @@ static void RunThread(std::unique_ptr args) { const auto entry = args->entry; void* arg = args->arg; PthreadPrivate* self = args->self; + if (!self->stackAddress) SetStackFromHost(self); currentThread = self; args.reset(); finishThread(self, entry(arg)); @@ -327,6 +349,7 @@ Pthread APS5_VABI scePthreadSelf() { adopted->threadId = std::this_thread::get_id(); adopted->_detached = true; adopted->references.store(1, std::memory_order_relaxed); + SetStackFromHost(adopted.get()); currentThread = adopted.release(); } #else @@ -335,6 +358,7 @@ Pthread APS5_VABI scePthreadSelf() { adoptedThread->_detached = true; adoptedThread->_adopted = true; adoptedThread->threadId = std::this_thread::get_id(); + SetStackFromHost(adoptedThread.get()); currentThread = adoptedThread.get(); } #endif diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 585745cbd..4290ec703 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -15,6 +15,23 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size); +} + +static bool StackContains(Pthread thread, const void* address) { + PthreadAttr attr = nullptr; + if (scePthreadAttrInit(&attr) != 0 || scePthreadAttrGet(thread, &attr) != 0) return false; + void* stack = nullptr; + size_t size = 0; + const bool queried = scePthreadAttrGetstackaddr(&attr, &stack) == 0 && scePthreadAttrGetstacksize(&attr, &size) == 0; + scePthreadAttrDestroy(&attr); + const auto begin = reinterpret_cast(stack); + const auto value = reinterpret_cast(address); + return queried && stack != nullptr && size != 0 && value >= begin && value - begin < size; } static constexpr int SCE_OK = 0; @@ -28,6 +45,7 @@ static void Require(bool value) { if (!value) std::abort(); } struct WorkerContext { Pthread thread = nullptr; Pthread selfFromWorker = nullptr; + bool workerStackReported = false; PthreadMutex* mutex = nullptr; int unlockResult = 0; }; @@ -35,6 +53,8 @@ struct WorkerContext { static void* APS5_VABI Worker(void* arg) { auto& context = *static_cast(arg); context.selfFromWorker = scePthreadSelf(); + int local = 0; + context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); scePthreadExit(reinterpret_cast(WorkerRetval)); return nullptr; @@ -44,6 +64,8 @@ int main() { const Pthread mainSelf = scePthreadSelf(); Require(mainSelf != nullptr); Require(scePthreadSelf() == mainSelf); + int local = 0; + Require(StackContains(mainSelf, &local)); Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); @@ -69,6 +91,7 @@ int main() { Require(context.selfFromWorker == context.thread); Require(context.selfFromWorker != mainSelf); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); + Require(context.workerStackReported); Require(scePthreadMutexUnlock(&mutex) == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); From 6ffc81a969043210bf5b00de4589fe61632660db Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 23:15:28 +0200 Subject: [PATCH 26/43] feat(msgdialog): add progress bar calls to libSceMsgDialog.native PPSA12544's Unity plugins (CommonDialog, PSN) import sceMsgDialogProgressBarInc, sceMsgDialogProgressBarSetMsg and sceMsgDialogProgressBarSetValue from libSceMsgDialog.native, which did not export them. As in libSceMsgDialog, they succeed while a dialog is running and return COMMON_DIALOG_ERROR_NOT_RUNNING otherwise; the native dialog finishes in sceMsgDialogOpen. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceMsgDialog.native/Export.cpp | 18 ++++++++++++++++++ core/libs/tests/GuestMsgDialogNative.cpp | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 027eff38d..678c37088 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -65,6 +65,24 @@ int APS5_VABI sceMsgDialogUpdateStatus(void) { return g_status.load(); } +int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { + (void)target; + (void)delta; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; +} + +int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { + (void)target; + (void)msg; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; +} + +int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { + (void)target; + (void)rate; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; +} + APS5_EXPORT("CWVW78Qc3fI", sceMsgDialogUnknown00); int APS5_VABI sceMsgDialogUnknown00(void) { NotImplemented_nid_no_patch("CWVW78Qc3fI"); diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 6e3b757e2..34f98d06b 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -10,6 +10,9 @@ int APS5_VABI sceMsgDialogUpdateStatus(void); int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result); int APS5_VABI sceMsgDialogClose(void); int APS5_VABI sceMsgDialogTerminate(void); +int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta); +int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg); +int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate); } namespace { @@ -33,8 +36,12 @@ int main() { Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); + Require(sceMsgDialogProgressBarSetValue(0, 50) == kErrNotRunning); Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogUpdateStatus() == kStatusFinished); + Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); + Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); + Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusFinished); From 9d4e6d2f28ad31750fcdecc63ce278d8d626f6cf Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 3 Oct 2026 21:29:28 +1300 Subject: [PATCH 27/43] feat(font): open system font sets from a font directory sceFontOpenFontSet failed for every set type, so PPSA21564 drew no text in system fonts (the title screen copyright line, credits, button guides). It now loads the set's primary font file from ANYPS5_SYSTEM_FONTS, or anyps5-fonts beside the executable: console files under their own names first, then openly licensed substitutes (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK). The PS5-only JG2 set types are accepted. System font handles use the system font context. Their faces are loaded by FreeType in host memory: about 1 MiB per CJK face does not fit the font memory the title sizes for the console's fonts, and sceFontCreateRenderer then failed with ALLOCATION_FAILED. Handles of the same font file share one copy of its bytes. --- core/libs/prx/libSceFont/CMakeLists.txt | 7 +- core/libs/prx/libSceFont/FontLibrary.cmake | 1 + .../prx/libSceFont/include/FontDriver.hpp | 1 + .../prx/libSceFont/include/FontInternal.hpp | 18 +- core/libs/prx/libSceFont/src/Layout.cpp | 10 +- core/libs/prx/libSceFont/src/Open.cpp | 87 +++---- core/libs/prx/libSceFont/src/State.cpp | 12 +- core/libs/prx/libSceFont/src/SystemFont.cpp | 131 +++++++++++ core/libs/prx/libSceFontFt/src/Driver.cpp | 56 ++++- core/libs/tests/GuestSystemFont.cpp | 218 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + docs/user/USAGE.md | 4 + 12 files changed, 492 insertions(+), 54 deletions(-) create mode 100644 core/libs/prx/libSceFont/src/SystemFont.cpp create mode 100644 core/libs/tests/GuestSystemFont.cpp diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 3ae1d162b..8866b00b4 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -12,8 +12,13 @@ if(BUILD_TESTING) target_include_directories(guest_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_tests PRIVATE libSceFont libSceFontFt libc) add_test(NAME guest_font COMMAND guest_font_tests) + add_test_executable(guest_system_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFont.cpp) + target_include_directories(guest_system_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_font_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_system_font_tests) + add_test(NAME guest_system_font COMMAND guest_system_font_tests) if(WIN32) - set_tests_properties(guest_font PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_font guest_system_font PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFont/FontLibrary.cmake b/core/libs/prx/libSceFont/FontLibrary.cmake index 26e287f39..d8ff9e0d3 100644 --- a/core/libs/prx/libSceFont/FontLibrary.cmake +++ b/core/libs/prx/libSceFont/FontLibrary.cmake @@ -7,6 +7,7 @@ function(add_sce_font_library target) ${fontDir}/src/Render.cpp ${fontDir}/src/State.cpp ${fontDir}/src/Style.cpp + ${fontDir}/src/SystemFont.cpp ${fontDir}/src/Text.cpp ${fontDir}/src/Unimplemented.cpp ) diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index e58ae256e..889c367dd 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -23,6 +23,7 @@ constexpr std::uint16_t STYLE_FRAME_MAGIC = 0x0F09; constexpr std::uint32_t LOCK_BIT = 0x80000000u; constexpr std::uint32_t OPEN_BIT = 0x40000000u; constexpr std::uint32_t COUNT_MASK = 0x0FFFFFFFu; +constexpr std::uint64_t SYSFONT_FLAG_SYSTEM_SET = 0x100u; constexpr std::size_t HORIZONTAL_LAYOUT_SIZE = 0x30; constexpr std::size_t HORIZONTAL_LINE_ADVANCE = 0x00; diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 426a21bee..b65648a0d 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -6,6 +6,9 @@ #include #include +#include +#include +#include #include #include @@ -21,7 +24,7 @@ constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; constexpr float POINTS_PER_INCH = 72.0f; struct FontState { - std::vector faceData; + std::shared_ptr> faceData; FT_Face face = nullptr; float scaleW = 16.0f; float scaleH = 16.0f; @@ -48,6 +51,11 @@ struct GeneratedGlyph { bool outlineInitialized = false; }; +struct SystemFontFile { + std::filesystem::path path; + std::uint32_t subFontIndex = 0; +}; + struct RenderSurfaceSystemUse { FontStyleFrame* styleframe; float catchedScale; @@ -70,12 +78,18 @@ void** EntryObjectSlot(FontCtxEntry* entry, std::uint32_t modeLow); FontCtxEntry* AcquireFontCtxEntry(void* ctx, std::uint32_t index, std::uint32_t modeLow, FontObj** outObject, std::uint32_t* outLockWord); void ReleaseFontCtxEntryLock(FontCtxEntry* entry, std::uint32_t modeLow, std::uint32_t lockWord); FontObj* FindSubFont(FontObj* head, std::uint32_t subFontIndex); +void* FontContext(const FontLibNative* library, const FontHandleNative* font); void ReleaseFontObjectsForHandle(FontHandleNative* font); FontState* TryGetState(FontHandle handle); FontState& ResetState(FontHandle handle); void RemoveState(FontHandle handle); -void LoadStateFace(FontState& state, std::vector bytes, std::uint32_t subFontIndex); +void LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); + +bool IsSystemFontSet(std::uint32_t fontSetType); +std::vector SystemFontCandidates(std::uint32_t fontSetType); +std::filesystem::path SystemFontDirectory(); +std::optional FindSystemFontFile(std::uint32_t fontSetType); void TrackGeneratedGlyph(FontGlyph glyph); bool ForgetGeneratedGlyph(FontGlyph glyph); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 7b76c46d0..68d242d53 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -212,7 +212,7 @@ int Font::ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* styl auto* font = GetNativeFont(handle); if (!font) return fail(); auto* library = static_cast(font->library); - if (!library || !library->external_fonts_ctx) return fail(); + if (!library || !FontContext(library, font)) return fail(); const SysDriver* driver = library->sys_driver; if (!driver || !driver->set_char_with_dpi || !driver->set_char_default_dpi || !driver->compute_layout || !style) return fail(); const std::uint32_t modeLow = font->flags & 0x0Fu; @@ -226,7 +226,7 @@ int Font::ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* styl if (static_cast(fontId) >= 0) { FontObj* head = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); if (!entry) return fail(); FontObj* match = FindSubFont(head, font->open_info.sub_font_index); int rc = SCE_FONT_ERROR_FATAL; @@ -288,7 +288,7 @@ int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, }; auto* font = GetNativeFont(handle); auto* library = font ? static_cast(font->library) : nullptr; - if (!library || !library->external_fonts_ctx || !library->sys_driver) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + if (!library || !FontContext(library, font) || !library->sys_driver) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); const SysDriver* driver = library->sys_driver; const std::uint32_t modeLow = font->flags & 0x0Fu; const std::uint32_t fontId = font->open_info.ctx_entry_index; @@ -303,7 +303,7 @@ int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, if (static_cast(fontId) >= 0) { FontObj* head = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); FontObj* obj = FindSubFont(head, font->open_info.sub_font_index); if (!entry || !obj) { if (entry) ReleaseFontCtxEntryLock(entry, modeLow, lockWord); @@ -454,7 +454,7 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe const StyleStateBlock* style = ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle); FontObj* obj = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); obj = FindSubFont(obj, font->open_info.sub_font_index); if (!entry || !obj) { if (entry) ReleaseFontCtxEntryLock(entry, modeLow, lockWord); diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index b92f7ebf9..3ed2eaa05 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -1,13 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include #include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -18,28 +20,11 @@ namespace { using namespace Font; -constexpr std::array SYSTEM_FONT_SET_TYPES = { - 0x18070043u, 0x18070044u, 0x18070045u, 0x18070047u, 0x18070053u, 0x18070054u, - 0x18070055u, 0x18070057u, 0x180700C3u, 0x180700C4u, 0x180700C5u, 0x180700C7u, - 0x18070444u, 0x18070447u, 0x18070454u, 0x18070457u, 0x180704C4u, 0x180704C7u, - 0x18071053u, 0x18071054u, 0x18071055u, 0x18071057u, 0x18071454u, 0x18071457u, - 0x18072444u, 0x18072447u, 0x180724C4u, 0x180724C7u, 0x18073454u, 0x18073457u, - 0x180734D4u, 0x180734D7u, 0x18078044u, 0x180780C4u, 0x18079054u, 0x1807A044u, - 0x1807A0C4u, 0x1807A444u, 0x1807A4C4u, 0x1807AC44u, 0x1807ACC4u, 0x1807B054u, - 0x1807B0D4u, 0x1807B454u, 0x1807B4D4u, 0x1807BC54u, 0x1807BCD4u, 0x18080444u, - 0x18080447u, 0x18080454u, 0x18080457u, 0x180804C4u, 0x180804C7u, 0x18081454u, - 0x18081457u, 0x18082444u, 0x18082447u, 0x180824C4u, 0x180824C7u, 0x18083454u, - 0x18083457u, 0x180834D4u, 0x180834D7u, 0x1808A444u, 0x1808A4C4u, 0x1808B454u, - 0x1808B4D4u, 0x180C8044u, 0x180C80C4u, 0x180C9054u, 0x180CA044u, 0x180CA0C4u, - 0x180CAC44u, 0x180CACC4u, 0x180CB054u, 0x180CB0D4u, 0x180CBC54u, 0x180CBCD4u, - 0x18170043u, 0x18170044u, 0x18170045u, 0x18170047u, 0x18170444u, 0x18170447u, - 0x18370044u, 0x18370047u, 0x18370444u, 0x18370447u, -}; - constexpr std::uint16_t HANDLE_OWNED = 0x10; struct OpenRequest { FontLibNative* library; + void* context; std::uint32_t modeLow; std::uint32_t driverMode; const void* source; @@ -111,7 +96,7 @@ void SetOpenFlags(FontLibNative* library, std::uint32_t modeLow) { if ((flags & secondBit) != 0) library->sysfont_flags |= 2; } -std::uint32_t ClaimExternalEntry(FontCtxHeader* header, const OpenRequest& request) { +std::uint32_t ClaimEntry(FontCtxHeader* header, const OpenRequest& request) { AcquireWordLock(header->lock_word); auto* entries = static_cast(header->base); std::uint32_t entryIndex = 0xFFFFFFFFu; @@ -140,7 +125,7 @@ std::uint32_t ClaimExternalEntry(FontCtxHeader* header, const OpenRequest& reque return entryIndex; } -int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, FontHandle* pFontHandle) { +int OpenFontInContext(const OpenRequest& request, std::uint32_t libraryLock, FontHandle* pFontHandle) { FontLibNative* library = request.library; FontHandle handle = PrepareHandle(library, *pFontHandle); if (!handle) { @@ -154,8 +139,8 @@ int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, Font *pFontHandle = nullptr; return rc; }; - auto* header = static_cast(library->external_fonts_ctx); - const std::uint32_t entryIndex = ClaimExternalEntry(header, request); + auto* header = static_cast(request.context); + const std::uint32_t entryIndex = ClaimEntry(header, request); if (entryIndex == 0xFFFFFFFFu) return fail(SCE_FONT_ERROR_FONT_OPEN_MAX); const std::uint32_t packedId = request.uniqueId == -1 ? entryIndex + 0x40000000u : (static_cast(request.uniqueId) | 0x80000000u); FontObj* fontObj = nullptr; @@ -183,6 +168,7 @@ int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, Font } if (needOpen) { SetOpenFlags(library, request.modeLow); + if (request.context == library->sysfonts_ctx) library->sysfont_flags |= SYSFONT_FLAG_SYSTEM_SET; rc = driver->open(library, request.driverMode, request.source, request.sourceSize, request.subFontIndex, packedId, &fontObj); library->sysfont_flags = 0; if (rc == SCE_FONT_OK) { @@ -221,11 +207,23 @@ int BeginOpen(FontLibrary library, FontHandle* pFontHandle, std::uint32_t& libra return rc; } -bool ReadFileBytes(const std::filesystem::path& path, std::vector& bytes) { +std::shared_ptr> ReadFileBytes(const std::filesystem::path& path) { std::ifstream file(path, std::ios::binary); - if (!file) return false; - bytes.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); - return !file.bad(); + if (!file) return nullptr; + std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (file.bad()) return nullptr; + return std::make_shared>(std::move(bytes)); +} + +std::shared_ptr> SystemFontBytes(const std::filesystem::path& path) { + static std::mutex mutex; + static std::map>> loaded; + std::lock_guard lock(mutex); + auto& slot = loaded[path]; + if (auto bytes = slot.lock()) return bytes; + auto bytes = ReadFileBytes(path); + slot = bytes; + return bytes; } void CopyStyleToCache(FontHandleNative* font) { @@ -250,11 +248,11 @@ int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* fontAddress if (pFontHandle) *pFontHandle = nullptr; return SCE_FONT_ERROR_INVALID_PARAMETER; } - const OpenRequest request{lib, 1, 1, fontAddress, fontSize, true, subFontIndex, uniqueId}; - const int rc = OpenExternalFont(request, libraryLock, pFontHandle); + const OpenRequest request{lib, lib->external_fonts_ctx, 1, 1, fontAddress, fontSize, true, subFontIndex, uniqueId}; + const int rc = OpenFontInContext(request, libraryLock, pFontHandle); if (rc != SCE_FONT_OK) return rc; const auto* bytes = static_cast(fontAddress); - LoadStateFace(ResetState(*pFontHandle), std::vector(bytes, bytes + fontSize), subFontIndex); + LoadStateFace(ResetState(*pFontHandle), std::make_shared>(bytes, bytes + fontSize), subFontIndex); return SCE_FONT_OK; } @@ -278,12 +276,11 @@ int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::ui *pFontHandle = nullptr; return SCE_FONT_ERROR_INVALID_PARAMETER; } - const OpenRequest request{lib, modeLow, modeLow + 4, hostPathString.c_str(), 0, false, subFontIndex, uniqueId}; - const int rc = OpenExternalFont(request, libraryLock, pFontHandle); + const OpenRequest request{lib, lib->external_fonts_ctx, modeLow, modeLow + 4, hostPathString.c_str(), 0, false, subFontIndex, uniqueId}; + const int rc = OpenFontInContext(request, libraryLock, pFontHandle); if (rc != SCE_FONT_OK) return rc; - std::vector bytes; FontState& state = ResetState(*pFontHandle); - if (ReadFileBytes(hostPath, bytes)) LoadStateFace(state, std::move(bytes), subFontIndex); + if (auto bytes = ReadFileBytes(hostPath)) LoadStateFace(state, std::move(bytes), subFontIndex); return SCE_FONT_OK; } @@ -309,7 +306,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon const std::uint32_t modeLow = source->flags & 0x0Fu; const std::uint32_t subFontIndex = source->open_info.sub_font_index; const std::uint32_t entryIndex = source->open_info.ctx_entry_index; - auto* header = static_cast(library->external_fonts_ctx); + auto* header = static_cast(FontContext(library, source)); if (!header) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); FontHandle target = setupFont; bool owned = false; @@ -371,7 +368,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon if (FontState* sourceState = TryGetState(fontHandle)) { targetState.scaleW = sourceState->scaleW; targetState.scaleH = sourceState->scaleH; - if (!sourceState->faceData.empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); } if (pFontHandle) *pFontHandle = target; return SCE_FONT_OK; @@ -385,6 +382,8 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, return SCE_FONT_ERROR_INVALID_LIBRARY; } if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; + const std::optional file = FindSystemFontFile(fontSetType); + APS5_LOG_OUT("fontSetType=0x%08X file=%s", fontSetType, file ? file->path.string().c_str() : "(none)"); std::uint32_t libraryLock = 0; if (!AcquireLibraryLock(lib, libraryLock)) { *pFontHandle = nullptr; @@ -399,10 +398,20 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, rc = SCE_FONT_ERROR_INVALID_MEMORY; } else if (!lib->sysfonts_ctx) { rc = SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; - } else if (std::find(SYSTEM_FONT_SET_TYPES.begin(), SYSTEM_FONT_SET_TYPES.end(), fontSetType) == SYSTEM_FONT_SET_TYPES.end()) { + } else if (!IsSystemFontSet(fontSetType)) { rc = SCE_FONT_ERROR_NO_SUPPORT_FONTSET; - } else { + } else if (!file) { rc = SCE_FONT_ERROR_FONT_OPEN_FAILED; + } else { + const std::string hostPathString = file->path.string(); + const OpenRequest request{lib, lib->sysfonts_ctx, openMode, openMode + 4, hostPathString.c_str(), 0, false, file->subFontIndex, static_cast(fontSetType)}; + const int openRc = OpenFontInContext(request, libraryLock, pFontHandle); + if (openRc == SCE_FONT_ERROR_FS_OPEN_FAILED || openRc == SCE_FONT_ERROR_NO_SUPPORT_FORMAT) return SCE_FONT_ERROR_FONT_OPEN_FAILED; + if (openRc != SCE_FONT_OK) return openRc; + GetNativeFont(*pFontHandle)->open_info.fontset_flags = fontSetType; + FontState& state = ResetState(*pFontHandle); + if (auto bytes = SystemFontBytes(file->path)) LoadStateFace(state, std::move(bytes), file->subFontIndex); + return SCE_FONT_OK; } ReleaseLibraryLock(lib, libraryLock); *pFontHandle = nullptr; diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index 58b7e0e49..6911fa24f 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -193,6 +193,10 @@ Font::FontObj* Font::FindSubFont(FontObj* head, std::uint32_t subFontIndex) { return nullptr; } +void* Font::FontContext(const FontLibNative* library, const FontHandleNative* font) { + return font->open_info.fontset_flags != 0 ? library->sysfonts_ctx : library->external_fonts_ctx; +} + void Font::ReleaseFontObjectsForHandle(FontHandleNative* font) { auto* library = static_cast(font->library); if (!library || library->magic != LIBRARY_MAGIC || !library->sys_driver || !library->sys_driver->close) return; @@ -201,7 +205,7 @@ void Font::ReleaseFontObjectsForHandle(FontHandleNative* font) { const std::uint32_t modeLow = font->flags & 0x0Fu; FontObj* head = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); if (!entry) return; FontObj* match = FindSubFont(head, font->open_info.sub_font_index); const std::uint32_t openFlag = lockWord & OPEN_BIT; @@ -243,16 +247,16 @@ void Font::RemoveState(FontHandle handle) { fontStates.erase(handle); } -void Font::LoadStateFace(FontState& state, std::vector bytes, std::uint32_t subFontIndex) { +void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { if (state.face) { FT_Done_Face(state.face); state.face = nullptr; } state.faceData = std::move(bytes); const FT_Library library = HostFreeTypeLibrary(); - if (!library || state.faceData.empty()) return; + if (!library || !state.faceData || state.faceData->empty()) return; FT_Face face = nullptr; - if (FT_New_Memory_Face(library, state.faceData.data(), static_cast(state.faceData.size()), static_cast(subFontIndex), &face) != 0) return; + if (FT_New_Memory_Face(library, state.faceData->data(), static_cast(state.faceData->size()), static_cast(subFontIndex), &face) != 0) return; FT_Select_Charmap(face, FT_ENCODING_UNICODE); state.face = face; } diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp new file mode 100644 index 000000000..bf1281380 --- /dev/null +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +constexpr std::array SYSTEM_FONT_SET_TYPES = { + 0x18070043u, 0x18070044u, 0x18070045u, 0x18070047u, 0x18070053u, 0x18070054u, + 0x18070055u, 0x18070057u, 0x180700C3u, 0x180700C4u, 0x180700C5u, 0x180700C7u, + 0x18070444u, 0x18070447u, 0x18070454u, 0x18070457u, 0x180704C4u, 0x180704C7u, + 0x18071053u, 0x18071054u, 0x18071055u, 0x18071057u, 0x18071454u, 0x18071457u, + 0x18072444u, 0x18072447u, 0x180724C4u, 0x180724C7u, 0x18073454u, 0x18073457u, + 0x180734D4u, 0x180734D7u, 0x18078044u, 0x180780C4u, 0x18079054u, 0x1807A044u, + 0x1807A0C4u, 0x1807A444u, 0x1807A4C4u, 0x1807AC44u, 0x1807ACC4u, 0x1807B054u, + 0x1807B0D4u, 0x1807B454u, 0x1807B4D4u, 0x1807BC54u, 0x1807BCD4u, 0x18080444u, + 0x18080447u, 0x18080454u, 0x18080457u, 0x180804C4u, 0x180804C7u, 0x18081454u, + 0x18081457u, 0x18082444u, 0x18082447u, 0x180824C4u, 0x180824C7u, 0x18083454u, + 0x18083457u, 0x180834D4u, 0x180834D7u, 0x1808A444u, 0x1808A4C4u, 0x1808B454u, + 0x1808B4D4u, 0x180C8044u, 0x180C80C4u, 0x180C9054u, 0x180CA044u, 0x180CA0C4u, + 0x180CAC44u, 0x180CACC4u, 0x180CB054u, 0x180CB0D4u, 0x180CBC54u, 0x180CBCD4u, + 0x18170043u, 0x18170044u, 0x18170045u, 0x18170047u, 0x18170444u, 0x18170447u, + 0x18370044u, 0x18370047u, 0x18370444u, 0x18370447u, 0x1A070543u, 0x1A070547u, + 0x1A070553u, 0x1A070557u, 0x1A0705C3u, 0x1A0705C7u, 0x1A071553u, 0x1A071557u, + 0x1A072543u, 0x1A072547u, 0x1A0725C3u, 0x1A0725C7u, 0x1A073553u, 0x1A073557u, + 0x1A0735D3u, 0x1A0735D7u, 0x1A080543u, 0x1A080547u, 0x1A080553u, 0x1A080557u, + 0x1A0805C3u, 0x1A0805C7u, 0x1A081553u, 0x1A081557u, 0x1A082543u, 0x1A082547u, + 0x1A0825C3u, 0x1A0825C7u, 0x1A083553u, 0x1A083557u, 0x1A0835D3u, 0x1A0835D7u, +}; + +enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb }; + +struct FamilyFiles { + std::array system; + std::array substitute; + std::uint32_t substituteSubFont; +}; + +constexpr std::array FAMILY_FILES = {{ + {{"SST-Light.otf", "SST-Roman.otf", "SST-Medium.otf", "SST-Bold.otf"}, + {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, + {{"SST-LightItalic.otf", "SST-Italic.otf", "SST-MediumItalic.otf", "SST-BoldItalic.otf"}, + {"NotoSans-LightItalic.ttf", "NotoSans-Italic.ttf", "NotoSans-MediumItalic.ttf", "NotoSans-BoldItalic.ttf"}, 0}, + {{"SSTTypewriter-Roman.otf", "SSTTypewriter-Roman.otf", "SSTTypewriter-Roman.otf", "SSTTypewriter-Bd.otf"}, + {"NotoSansMono-Light.ttf", "NotoSansMono-Regular.ttf", "NotoSansMono-Medium.ttf", "NotoSansMono-Bold.ttf"}, 0}, + {{"SSTVietnamese-Light.otf", "SSTVietnamese-Roman.otf", "SSTVietnamese-Medium.otf", "SSTVietnamese-Bold.otf"}, + {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, + {{"SSTThai-Light.otf", "SSTThai-Roman.otf", "SSTThai-Medium.otf", "SSTThai-Bold.otf"}, + {"NotoSansThai-Light.ttf", "NotoSansThai-Regular.ttf", "NotoSansThai-Medium.ttf", "NotoSansThai-Bold.ttf"}, 0}, + {{"SSTJpPro-Regular.otf", "SSTJpPro-Regular.otf", "SSTJpPro-Regular.otf", "SSTJpPro-Bold.otf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, + {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, +}}; + +Family FamilyOf(std::uint32_t fontSetType) { + const std::uint32_t region = (fontSetType >> 16) & 0xFFu; + const std::uint32_t scripts = (fontSetType >> 8) & 0xFEu; + const bool vietnamese = (fontSetType & 0x10u) != 0; + switch (region) { + case 0x17: return Family::LatinItalic; + case 0x37: return Family::Typewriter; + case 0x08: return scripts == 0x04 && vietnamese ? Family::Vietnamese : Family::Japanese; + case 0x0C: return Family::ChineseGb; + default: + if (scripts == 0x10) return Family::Thai; + if (scripts == 0 && vietnamese) return Family::Vietnamese; + return Family::Latin; + } +} + +std::size_t WeightOf(std::uint32_t fontSetType) { + switch (fontSetType & 0x0Fu) { + case 0x3: return 0; + case 0x5: return 2; + case 0x7: return 3; + default: return 1; + } +} + +std::filesystem::path ExecutableDirectory() { +#ifdef _WIN32 + wchar_t module[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); + if (length == 0 || length == MAX_PATH) throw std::runtime_error("libSceFont: cannot locate the executable"); + return std::filesystem::path(module).parent_path(); +#else + return std::filesystem::read_symlink("/proc/self/exe").parent_path(); +#endif +} + +} + +bool Font::IsSystemFontSet(std::uint32_t fontSetType) { + return std::find(SYSTEM_FONT_SET_TYPES.begin(), SYSTEM_FONT_SET_TYPES.end(), fontSetType) != SYSTEM_FONT_SET_TYPES.end(); +} + +std::vector Font::SystemFontCandidates(std::uint32_t fontSetType) { + if (!IsSystemFontSet(fontSetType)) return {}; + const FamilyFiles& files = FAMILY_FILES[static_cast(FamilyOf(fontSetType))]; + const std::size_t weight = WeightOf(fontSetType); + return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; +} + +std::filesystem::path Font::SystemFontDirectory() { + const char* configured = std::getenv("ANYPS5_SYSTEM_FONTS"); + if (!configured || configured[0] == '\0') return ExecutableDirectory() / "anyps5-fonts"; + const std::filesystem::path directory(configured); + std::error_code error; + if (!std::filesystem::is_directory(directory, error)) throw std::runtime_error("libSceFont: ANYPS5_SYSTEM_FONTS is not a directory: " + directory.string()); + return directory; +} + +std::optional Font::FindSystemFontFile(std::uint32_t fontSetType) { + const std::vector candidates = SystemFontCandidates(fontSetType); + if (candidates.empty()) return std::nullopt; + const std::filesystem::path directory = SystemFontDirectory(); + for (const SystemFontFile& candidate : candidates) { + std::error_code error; + const std::filesystem::path path = directory / candidate.path; + if (std::filesystem::is_regular_file(path, error)) return SystemFontFile{path, candidate.subFontIndex}; + } + return std::nullopt; +} diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 71c1ff144..ad09bd5a2 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,8 @@ struct FtLibraryContext { const FontMemoryInterface* iface; FT_Memory memory; FT_Library library; + FT_Memory hostMemory; + FT_Library hostLibrary; }; void* FtAlloc(FT_Memory memory, long size) { @@ -58,6 +61,42 @@ void* FtRealloc(FT_Memory memory, long currentSize, long newSize, void* block) { return result; } +void* HostAlloc(FT_Memory, long size) { + return size > 0 ? std::malloc(static_cast(size)) : nullptr; +} + +void HostFree(FT_Memory, void* block) { + std::free(block); +} + +void* HostRealloc(FT_Memory, long, long newSize, void* block) { + if (newSize <= 0) { + std::free(block); + return nullptr; + } + return std::realloc(block, static_cast(newSize)); +} + +FT_Library SystemFontLibrary(FtLibraryContext* ctx) { + if (ctx->hostLibrary) return ctx->hostLibrary; + auto* hostMemory = static_cast(ctx->iface->alloc(ctx->allocCtx, sizeof(FT_MemoryRec_))); + if (!hostMemory) return nullptr; + std::memset(hostMemory, 0, sizeof(*hostMemory)); + hostMemory->user = ctx; + hostMemory->alloc = &HostAlloc; + hostMemory->free = &HostFree; + hostMemory->realloc = &HostRealloc; + FT_Library hostLibrary = nullptr; + if (FT_New_Library(hostMemory, &hostLibrary) != 0 || !hostLibrary) { + ctx->iface->dealloc(ctx->allocCtx, hostMemory); + return nullptr; + } + FT_Add_Default_Modules(hostLibrary); + ctx->hostMemory = hostMemory; + ctx->hostLibrary = hostLibrary; + return hostLibrary; +} + void WriteU16(std::uint8_t* out, std::size_t offset, std::uint16_t value) { std::memcpy(out + offset, &value, sizeof(value)); } @@ -131,6 +170,14 @@ int APS5_VABI LibraryTerm(FontLibNative* library) { FT_Done_Library(ctx->library); ctx->library = nullptr; } + if (ctx->hostLibrary) { + FT_Done_Library(ctx->hostLibrary); + ctx->hostLibrary = nullptr; + } + if (ctx->hostMemory) { + freeFn(library->alloc_ctx, ctx->hostMemory); + ctx->hostMemory = nullptr; + } if (ctx->memory) { freeFn(library->alloc_ctx, ctx->memory); ctx->memory = nullptr; @@ -162,12 +209,15 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const } auto* ctx = static_cast(library->fontset_registry); if (!ctx || !ctx->library) return SCE_FONT_ERROR_INVALID_LIBRARY; + const bool systemSet = (library->sysfont_flags & SYSFONT_FLAG_SYSTEM_SET) != 0; + FT_Library* ftLibrary = systemSet ? &ctx->hostLibrary : &ctx->library; + if (systemSet && !SystemFontLibrary(ctx)) return SCE_FONT_ERROR_ALLOCATION_FAILED; FT_Face face = nullptr; FT_Error error; if (fromMemory) { - error = FT_New_Memory_Face(ctx->library, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); + error = FT_New_Memory_Face(*ftLibrary, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); } else { - error = FT_New_Face(ctx->library, static_cast(fontAddress), static_cast(subFontIndex), &face); + error = FT_New_Face(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); } if (error != 0 || !face) { if (fromMemory || error == FT_Err_Unknown_File_Format) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; @@ -186,7 +236,7 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const obj->next = *inoutFontObj; if (obj->next) obj->next->prev = obj; obj->ft_face = face; - obj->ft_ctx_0x58 = &ctx->library; + obj->ft_ctx_0x58 = ftLibrary; *inoutFontObj = obj; return SCE_FONT_OK; } diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp new file mode 100644 index 000000000..53f205e5d --- /dev/null +++ b/core/libs/tests/GuestSystemFont.cpp @@ -0,0 +1,218 @@ +#include "prx/libSceFont/include/FontTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceFontMemoryInit(FontMemory*, void*, std::uint32_t, const FontMemoryInterface*, void*, FontMemoryDestroyFunction, void*); +int APS5_VABI sceFontCreateLibrary(const FontMemory*, const void*, FontLibrary*); +int APS5_VABI sceFontDestroyLibrary(FontLibrary*); +int APS5_VABI sceFontSupportSystemFonts(FontLibrary); +int APS5_VABI sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); +int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontInstance(FontHandle, FontHandle, FontHandle*); +int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); +int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle, std::uint32_t, FontGlyphMetrics*); +int APS5_VABI sceFontGetHorizontalLayout(FontHandle, FontHorizontalLayout*); +const void* APS5_VABI sceFontSelectLibraryFt(int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "System font check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static int allocations = 0; +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + ++allocations; + return std::malloc(size); +} +static void APS5_VABI Release(void*, void* pointer) { + if (pointer) --allocations; + std::free(pointer); +} + +static void Put16(std::vector& out, std::uint32_t value) { + out.push_back(static_cast(value >> 8)); + out.push_back(static_cast(value)); +} + +static void Put32(std::vector& out, std::uint32_t value) { + Put16(out, value >> 16); + Put16(out, value & 0xFFFFu); +} + +static void PutAll16(std::vector& out, std::initializer_list values) { + for (const int value : values) Put16(out, static_cast(static_cast(value))); +} + +static std::vector SquareGlyphFont(std::uint32_t codepoint, int width) { + std::vector head; + Put32(head, 0x00010000u); + Put32(head, 0x00010000u); + Put32(head, 0); + Put32(head, 0x5F0F3CF5u); + PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); + std::vector hhea; + Put32(hhea, 0x00010000u); + PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); + std::vector maxp; + Put32(maxp, 0x00010000u); + PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + std::vector hmtx; + PutAll16(hmtx, {width, 0, width, 100}); + std::vector glyf; + PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); + glyf.insert(glyf.end(), {1, 1, 1, 1}); + PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); + std::vector loca; + PutAll16(loca, {0, 0, static_cast(glyf.size() / 2)}); + std::vector cmap; + PutAll16(cmap, {0, 1, 3, 1}); + Put32(cmap, 12); + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast(codepoint), 0xFFFF, 0, static_cast(codepoint), 0xFFFF, 1 - static_cast(codepoint), 1, 0, 0}); + const std::vector*>> tables = { + {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + std::vector font; + Put32(font, 0x00010000u); + PutAll16(font, {static_cast(tables.size()), 64, 2, static_cast(tables.size()) * 16 - 64}); + std::uint32_t offset = 12 + static_cast(tables.size()) * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag, tag + 4); + Put32(font, 0); + Put32(font, offset); + Put32(font, static_cast(data->size())); + offset += (static_cast(data->size()) + 3u) & ~3u; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data->begin(), data->end()); + font.resize((font.size() + 3u) & ~std::size_t{3}); + } + return font; +} + +static void WriteFile(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(static_cast(file)); +} + +static void SetFontDirectory(const std::filesystem::path& directory) { +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_SYSTEM_FONTS", directory.string().c_str()) == 0); +#else + Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.string().c_str(), 1) == 0); +#endif +} + +static float AdvanceOf(FontHandle font, std::uint32_t code) { + FontGlyphMetrics metrics{}; + Require(sceFontGetCharGlyphMetrics(font, code, &metrics) == SCE_FONT_OK); + return metrics.Horizontal.advance; +} + +int main() { + constexpr std::uint32_t EuropeanLight = 0x180700C3u; + constexpr std::uint32_t EuropeanBold = 0x180700C7u; + constexpr std::uint32_t EuropeanItalic = 0x18170044u; + constexpr std::uint32_t ThaiMedium = 0x18071055u; + constexpr std::uint32_t VietnameseBold = 0x18070057u; + constexpr std::uint32_t JapaneseJg2Light = 0x1A0835D3u; + constexpr std::uint32_t ChineseGb = 0x180CB0D4u; + const std::filesystem::path root = std::filesystem::temp_directory_path() / "anyps5_guest_system_font"; + std::filesystem::remove_all(root); + const std::filesystem::path empty = root / "empty"; + const std::filesystem::path fonts = root / "fonts"; + std::filesystem::create_directories(empty); + std::filesystem::create_directories(fonts); + WriteFile(fonts / "SST-Bold.otf", SquareGlyphFont('A', 700)); + WriteFile(fonts / "NotoSans-Bold.ttf", SquareGlyphFont('A', 300)); + WriteFile(fonts / "NotoSans-Light.ttf", SquareGlyphFont('A', 500)); + WriteFile(fonts / "SST-Italic.otf", SquareGlyphFont('A', 520)); + WriteFile(fonts / "NotoSansThai-Medium.ttf", SquareGlyphFont(0x0E01, 540)); + WriteFile(fonts / "SSTVietnamese-Bold.otf", SquareGlyphFont(0x1EA0, 560)); + WriteFile(fonts / "NotoSansCJK-Light.ttc", SquareGlyphFont(0x3042, 580)); + + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportSystemFonts(library) == SCE_FONT_OK); + + FontHandle font = nullptr; + SetFontDirectory(empty); + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + SetFontDirectory(root / "missing"); + bool threw = false; + try { + sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + + SetFontDirectory(fonts); + const int beforeOpen = allocations; + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_OK && font != nullptr); + Require(allocations - beforeOpen == 3); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(font, 'A') == 70.0f); + FontGlyphMetrics metrics{}; + Require(sceFontGetCharGlyphMetrics(font, 'B', &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FontHorizontalLayout layout{}; + Require(sceFontGetHorizontalLayout(font, &layout) == SCE_FONT_OK && layout.baselineOffset > 0.0f && layout.lineAdvance >= layout.baselineOffset); + + FontHandle again = nullptr; + Require(sceFontOpenFontSet(library, EuropeanBold, 2, nullptr, &again) == SCE_FONT_OK && again != font); + FontHandle instance = nullptr; + Require(sceFontOpenFontInstance(font, nullptr, &instance) == SCE_FONT_OK && instance != nullptr); + Require(sceFontGetHorizontalLayout(instance, &layout) == SCE_FONT_OK && layout.baselineOffset > 0.0f); + + const std::vector>> expected = { + {EuropeanLight, {'A', 50.0f}}, {EuropeanItalic, {'A', 52.0f}}, {ThaiMedium, {0x0E01, 54.0f}}, + {VietnameseBold, {0x1EA0, 56.0f}}, {JapaneseJg2Light, {0x3042, 58.0f}}}; + for (const auto& [type, glyph] : expected) { + FontHandle set = nullptr; + Require(sceFontOpenFontSet(library, type, 1, nullptr, &set) == SCE_FONT_OK); + Require(sceFontSetScalePixel(set, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(set, glyph.first) == glyph.second); + Require(sceFontCloseFont(set) == SCE_FONT_OK); + } + Require(sceFontCloseFont(again) == SCE_FONT_OK); + FontHandle missing = nullptr; + Require(sceFontOpenFontSet(library, ChineseGb, 1, nullptr, &missing) == SCE_FONT_ERROR_FONT_OPEN_FAILED && missing == nullptr); + + std::filesystem::remove(fonts / "SST-Bold.otf"); + FontHandle substitute = nullptr; + Require(sceFontOpenFontSet(library, EuropeanBold, 3, nullptr, &substitute) == SCE_FONT_OK); + Require(sceFontSetScalePixel(substitute, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(substitute, 'A') == 30.0f); + Require(AdvanceOf(font, 'A') == 70.0f); + + Require(sceFontSupportExternalFonts(library, 2, 0x52) == SCE_FONT_OK); + const std::vector external = SquareGlyphFont('A', 900); + FontHandle memoryFont = nullptr; + Require(sceFontOpenFontMemory(library, external.data(), static_cast(external.size()), nullptr, &memoryFont) == SCE_FONT_OK); + Require(sceFontSetScalePixel(memoryFont, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(memoryFont, 'A') == 90.0f); + Require(sceFontGetHorizontalLayout(memoryFont, &layout) == SCE_FONT_OK); + + for (FontHandle handle : {font, instance, substitute, memoryFont}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(allocations == 0); + std::filesystem::remove_all(root); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c457ddd14..5fd46685b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -127,3 +127,4 @@ Throughout the project, every function at every stage either **does exactly what - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped +- [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 7cb429298..999714177 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -85,6 +85,10 @@ Windows PowerShell: .\app.exe ``` +### System fonts + +Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts. + ## Exit codes `0`: conversion succeeded. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. From 669c7a40fa2e7a8ade263c3110127a74e09903ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Mon, 5 Oct 2026 12:01:12 -0300 Subject: [PATCH 28/43] feat(http): implement response header parsing --- core/libs/prx/libSceHttp/CMakeLists.txt | 7 +- core/libs/prx/libSceHttp/Export.cpp | 66 +++++++++++- core/libs/tests/GuestHttpResponseHeader.cpp | 109 ++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestHttpResponseHeader.cpp diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 473651532..900e9ce2e 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -13,8 +13,13 @@ if(BUILD_TESTING) target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_http_tests PRIVATE libSceHttp) add_test(NAME guest_http COMMAND guest_http_tests) + add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp) + target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_response_header_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_response_header_tests) + add_test(NAME guest_http_response_header COMMAND guest_http_response_header_tests) if(WIN32) - set_tests_properties(guest_http PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_http guest_http_response_header PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 0bbfa9e09..fa00ad2d7 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -4,6 +4,9 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" #include +#include +#include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. @@ -257,9 +260,66 @@ int APS5_VABI sceHttpSetRequestStatusCallback(void) { return 0; } -int APS5_VABI sceHttpParseResponseHeader(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerLen, const char* fieldStr, + const char** fieldValue, std::size_t* valueLen) { + constexpr int invalidResponse = static_cast(0x80432060); + constexpr int invalidValue = static_cast(0x804321FE); + constexpr int notFound = static_cast(0x80432025); + if (!header) return invalidResponse; + if (!fieldStr || !fieldValue || !valueLen) return invalidValue; + + std::size_t fieldLen = 0; + while (fieldLen < 0xfff && fieldStr[fieldLen] != '\0') ++fieldLen; + const std::string_view input(header, headerLen); + const auto isSpace = [](char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }; + const auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; }; + std::size_t start = 0; + bool found = false; + while (start < headerLen) { + if (!isSpace(input[start]) && fieldLen < headerLen - start && input[start + fieldLen] == ':') { + std::size_t i = 0; + while (i < fieldLen && lower(input[start + i]) == lower(fieldStr[i])) ++i; + if (i == fieldLen) { + start += fieldLen + 1; + found = true; + break; + } + } + const auto newline = input.find('\n', start); + if (newline == std::string_view::npos) break; + start = newline + 1; + } + if (!found) return notFound; + + while (start < headerLen && isSpace(input[start])) { + if (input[start++] == '\n') break; + } + std::size_t end = headerLen; + std::size_t consumed = headerLen; + std::size_t scan = start; + while (scan < headerLen) { + const auto newline = input.find('\n', scan); + if (newline == std::string_view::npos) break; + const auto next = newline + 1; + if (next < headerLen && (input[next] == ' ' || input[next] == '\t')) { + scan = next; + continue; + } + end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline; + consumed = next; + break; + } + if (end == start) { + *fieldValue = nullptr; + *valueLen = 0; + return 0; + } + if (consumed > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("sceHttpParseResponseHeader: consumed byte count exceeds int"); + } + *fieldValue = header + start; + *valueLen = end - start; + return static_cast(consumed); } int APS5_VABI sceHttpCreateRequest2(int conn_id, const char* method, const char* path, uint64_t content_length) { diff --git a/core/libs/tests/GuestHttpResponseHeader.cpp b/core/libs/tests/GuestHttpResponseHeader.cpp new file mode 100644 index 000000000..e02738dbc --- /dev/null +++ b/core/libs/tests/GuestHttpResponseHeader.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI sceHttpParseResponseHeader(const char*, std::size_t, const char*, const char**, std::size_t*); + +static void Require(bool value) { + if (!value) { + std::fputs("HTTP response header check failed\n", stderr); + std::abort(); + } +} + +static void Check(std::string_view header, const char* field, std::size_t offset, std::string_view expected, int consumed) { + const char* value = nullptr; + std::size_t length = 999; + const int result = sceHttpParseResponseHeader(header.data(), header.size(), field, &value, &length); + if (result != consumed) { + std::fprintf(stderr, "Field %s: consumed %d bytes, expected %d\n", field, result, consumed); + } + Require(result == consumed); + Require(length == expected.size()); + Require(value == (expected.empty() ? nullptr : header.data() + offset)); + if (length != 0) Require(std::memcmp(value, expected.data(), length) == 0); +} + +static void CheckGuardPage() { +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + const std::size_t pageSize = info.dwPageSize; + auto* pages = static_cast(VirtualAlloc(nullptr, 2 * pageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(pages != nullptr); + DWORD oldProtection = 0; + Require(VirtualProtect(pages + pageSize, pageSize, PAGE_NOACCESS, &oldProtection) != 0); +#else + const long hostPageSize = sysconf(_SC_PAGESIZE); + Require(hostPageSize > 0); + const std::size_t pageSize = static_cast(hostPageSize); + auto* pages = static_cast(mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(pages != MAP_FAILED); + Require(mprotect(pages + pageSize, pageSize, PROT_NONE) == 0); +#endif + constexpr std::string_view text = "X: abc\r\n\tdef\r\n"; + char* header = pages + pageSize - text.size(); + std::memcpy(header, text.data(), text.size()); + Check({header, text.size()}, "x", 3, "abc\r\n\tdef", static_cast(text.size())); + Check({header, 6}, "x", 3, "abc", 6); + const char* value = header; + std::size_t length = 123; + Require(sceHttpParseResponseHeader(header, text.size(), "not-present", &value, &length) == static_cast(0x80432025)); + Require(value == header && length == 123); + Require(sceHttpParseResponseHeader(pages + pageSize, 0, "x", &value, &length) == static_cast(0x80432025)); + pages[pageSize - 1] = 'X'; + Require(sceHttpParseResponseHeader(pages + pageSize - 1, 1, "x", &value, &length) == static_cast(0x80432025)); +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, 2 * pageSize) == 0); +#endif +} + +int main() { + constexpr int invalidResponse = static_cast(0x80432060); + constexpr int invalidValue = static_cast(0x804321FE); + constexpr int notFound = static_cast(0x80432025); + Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 30); + Check("X: first\r\nX: second\r\n", "X", 3, "first", 10); + Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 17); + Check("X: no-newline", "X", 3, "no-newline", 13); + Check("X: value\r", "X", 3, "value\r", 9); + Check("X:", "X", 2, "", 0); + Check("X: \t", "X", 4, "", 0); + Check("X:\r\n\r\n", "X", 5, "", 0); + Check("X:\r\nnext\r\n", "X", 4, "next", 10); + Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right", 43); + Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b", 3), 7); + + const char* marker = "unchanged"; + const char* value = marker; + std::size_t length = 456; + Require(sceHttpParseResponseHeader(nullptr, 1, "X", &value, &length) == invalidResponse); + Require(sceHttpParseResponseHeader(nullptr, 0, nullptr, nullptr, nullptr) == invalidResponse); + Require(sceHttpParseResponseHeader("X: yes", 6, nullptr, &value, &length) == invalidValue); + Require(sceHttpParseResponseHeader("X: yes", 6, "X", nullptr, &length) == invalidValue); + Require(sceHttpParseResponseHeader("X: yes", 6, "X", &value, nullptr) == invalidValue); + Require(sceHttpParseResponseHeader("X: yes", 0, "X", &value, &length) == notFound); + Require(sceHttpParseResponseHeader("X: yes", 1, "X", &value, &length) == notFound); + Require(sceHttpParseResponseHeader("X: yes", 6, "Longer-Field", &value, &length) == notFound); + Require(sceHttpParseResponseHeader("X: yes", 6, "X:", &value, &length) == notFound); + Require(value == marker && length == 456); + + std::string field(0xfff, 'A'); + std::string header = field + ": value\n"; + field += "ignored"; + Check(header, field.c_str(), 0x1000 + 1, "value", static_cast(header.size())); + CheckGuardPage(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..b41dae96e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -78,7 +78,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted -- [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature From 241faf881ecebc90724a4d1821f7e1fa7e64aa90 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:30:18 +0200 Subject: [PATCH 29/43] feat(agc): present DCC display buffers whose keys are uncompressed or fast-cleared sceVideoOutRegisterBuffers2 threw "DCC presentation is not implemented" for category 1 (DCC-compressed) buffers, which PPSA26344 registers. Such a buffer now needs DCC metadata and the tiled layout, and dcc_control may only use the CB_DCC_CONTROL block layout bits 0x10026c. At a flip the keys are read behind their recorded writers, one byte per 256 bytes as in the driver's DCC model. Uncompressed keys present the stored texels as before; a uniform clear code presents its color with a GPU clear of the scaler's source image; a pending storage image is used when it serves the keys. Mixed or unreadable keys, clear codes the display format has no DCC encoding for, register-clear keys over a pending image and a clear color wider than 32 bits throw. Buffers without DCC take the old path. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 ++++ .../Execution/include/DisplayBuffer.hpp | 8 ++ .../Execution/include/PresentationScaler.hpp | 1 + .../Execution/include/VulkanDevice.hpp | 2 +- .../Execution/src/DisplayBuffer.cpp | 63 ++++++++++---- .../Execution/src/PresentationScaler.cpp | 20 +++++ .../Execution/src/VulkanDevice.cpp | 63 ++++++++++++-- .../Graphics/include/DccMetadata.hpp | 1 + .../Graphics/src/DccMetadata.cpp | 4 + .../tests/DisplayBufferDcc.cpp | 83 +++++++++++++++++++ .../libSceVideoOut/include/VideoOutDriver.hpp | 1 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 15 +++- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 52 ++++++++++++ docs/dev/TechnicalDebt.md | 2 + 14 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b373847f0..65091c14a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -303,6 +303,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_display_buffer_dcc_tests EXCLUDE_FROM_ALL tests/DisplayBufferDcc.cpp) +target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_link_libraries(agc_display_buffer_dcc_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_display_buffer_dcc_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_display_buffer_dcc_tests) +if(BUILD_TESTING) + set_target_properties(agc_display_buffer_dcc_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_display_buffer_dcc COMMAND agc_display_buffer_dcc_tests) + if(WIN32) + set_tests_properties(agc_display_buffer_dcc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_validate_program_tests EXCLUDE_FROM_ALL tests/ValidateProgram.cpp) target_link_libraries(agc_validate_program_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_validate_program_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index b7f70c3d8..5c3ef004f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DISPLAYBUFFER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DISPLAYBUFFER_HPP +#include #include #include #include @@ -8,6 +9,10 @@ namespace AgcDriver { +namespace Graphics { +enum class DccKeys; +} + struct DisplayBuffer { std::uint64_t address; std::uint64_t pixelFormat; @@ -15,11 +20,14 @@ struct DisplayBuffer { std::uint32_t height; std::uint32_t tilingMode = 0; std::uint32_t pitchInPixel = 0; + std::uint64_t dccAddress = 0; + std::uint64_t dccClearColor = 0; }; std::size_t DisplayBufferSize(const DisplayBuffer& buffer); std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source); std::vector ReadDisplayBuffer(const DisplayBuffer& buffer); +std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index d56076311..35e8ed497 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -17,6 +17,7 @@ public: void EnsureSourceImage(std::uint32_t width, std::uint32_t height); void RecordUpload(VkCommandBuffer commands, VkBuffer uploadBuffer); + void RecordClear(VkCommandBuffer commands, const VkClearColorValue& color); void RecordImage(VkCommandBuffer commands, VkImage image); // Fills the source image from another image of the same extent (any blittable format, layout // unchanged), leaving the source image ready to blit or read back. diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index dc519ff5b..0064178b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -307,7 +307,7 @@ private: // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false); + bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr); struct State; std::unique_ptr state; std::uint64_t serial; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index 837b13f02..459b6e228 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include #include #include #include @@ -28,6 +31,25 @@ std::uint32_t tileOffset(std::uint32_t x, std::uint32_t y) { return ((y << 4u) & 0x0070u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 9u) & 0x1000u) ^ ((y << 8u) & 0x4000u) ^ ((x << 2u) & 0x000cu) ^ ((x << 5u) & 0x0380u) ^ ((x << 4u) & 0x0400u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0xa000u); } +void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit) { + if (tenBit) { + std::uint32_t word = 0; + std::memcpy(&word, texel, 4); + const auto red = static_cast((word >> 2u) & 0xffu); + const auto green = static_cast((word >> 12u) & 0xffu); + const auto blue = static_cast((word >> 22u) & 0xffu); + pixel[0] = rgba ? blue : red; + pixel[1] = green; + pixel[2] = rgba ? red : blue; + pixel[3] = std::byte{0xff}; + return; + } + pixel[0] = texel[rgba ? 2 : 0]; + pixel[1] = texel[1]; + pixel[2] = texel[rgba ? 0 : 2]; + pixel[3] = texel[3]; +} + } std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { @@ -40,6 +62,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment"); require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode"); require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported"); + require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata"); + require(buffer.dccAddress != 0 || buffer.dccClearColor == 0, "VideoOut: a DCC clear color needs DCC metadata"); const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; require(pitch >= buffer.width && pitch <= 16384, "VideoOut: invalid linear display pitch"); const auto size = buffer.tilingMode == 1 ? static_cast(pitch) * buffer.height * 4u @@ -64,28 +88,35 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa const auto tiled = buffer.tilingMode == 1 ? (static_cast(y) * pitch + x) * 4u : (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); const auto linear = (static_cast(y) * buffer.width + x) * 4; - if (tenBit) { - std::uint32_t word = 0; - std::memcpy(&word, source.data() + tiled, 4); - const auto red = static_cast((word >> 2u) & 0xffu); - const auto green = static_cast((word >> 12u) & 0xffu); - const auto blue = static_cast((word >> 22u) & 0xffu); - pixels[linear] = rgba ? blue : red; - pixels[linear + 1] = green; - pixels[linear + 2] = rgba ? red : blue; - pixels[linear + 3] = std::byte{0xff}; - continue; - } - pixels[linear] = source[tiled + (rgba ? 2 : 0)]; - pixels[linear + 1] = source[tiled + 1]; - pixels[linear + 2] = source[tiled + (rgba ? 0 : 2)]; - pixels[linear + 3] = source[tiled + 3]; + decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit); } } timing.Mark("detile_convert"); return pixels; } +std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys) { + static_cast(DisplayBufferSize(buffer)); + require(buffer.dccAddress != 0, "VideoOut: a display buffer without DCC metadata has no fast-clear value"); + std::array texel{}; + if (keys == Graphics::DccKeys::ClearRegister) { + if ((buffer.dccClearColor >> 32u) != 0) { + char message[160]; + std::snprintf(message, sizeof(message), "VideoOut: DCC register clear color 0x%016llx of display buffer 0x%llx does not fit its 32-bit texel", static_cast(buffer.dccClearColor), static_cast(buffer.address)); + throw std::runtime_error(message); + } + const auto word = static_cast(buffer.dccClearColor); + std::memcpy(texel.data(), &word, texel.size()); + } else if (!Graphics::IsDccClear(keys) || !Graphics::FillDccClear(DisplayTexelFormat(buffer.pixelFormat), keys, true, texel)) { + char message[256]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx (format 0x%016llx) reads %s DCC keys at 0x%llx: only uncompressed keys or a uniform clear code that the display format encodes can be presented", static_cast(buffer.address), static_cast(buffer.pixelFormat), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress)); + throw std::runtime_error(message); + } + std::array pixel{}; + decodeTexel(texel.data(), pixel.data(), baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8, (buffer.pixelFormat & PixelFormatUnormBit) != 0); + return pixel; +} + std::vector ReadDisplayBuffer(const DisplayBuffer& buffer) { const auto size = DisplayBufferSize(buffer); static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index ecea37cbe..354b2f0cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -78,6 +78,26 @@ void PresentationScaler::RecordUpload(VkCommandBuffer commands, VkBuffer uploadB pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); } +void PresentationScaler::RecordClear(VkCommandBuffer commands, const VkClearColorValue& color) { + Graphics::Require(sourceImage != VK_NULL_HANDLE, "presentation source image has not been created"); + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = sourceImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + context.Function("vkCmdClearColorImage")(commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &color, 1, &barrier.subresourceRange); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter) { RecordBlitInto(commands, image, layout, filter, sourceWidth, sourceHeight); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b040bbb41..8eca21737 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" @@ -1822,6 +1823,41 @@ std::shared_ptr PresentableResident(const Graphics::Co return resident; } +Graphics::DccKeys DisplayKeys(const DisplayBuffer& buffer, std::size_t bytes) { + const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, bytes); + if (keys != Graphics::DccKeys::Uncompressed && !Graphics::IsDccClear(keys)) { + char message[256]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast(buffer.address), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress)); + throw std::runtime_error(message); + } + return keys; +} + +bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer, std::size_t bytes) { + const auto keys = DisplayKeys(buffer, bytes); + if (keys == Graphics::DccKeys::ClearRegister) { + char message[320]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads register-clear DCC keys at 0x%llx over the pending image 0x%llx (DCC 0x%llx, filled keys %s): whether its results precede the clear is not modeled", static_cast(buffer.address), static_cast(buffer.dccAddress), static_cast(resident.Descriptor().baseAddress), static_cast(resident.Descriptor().dccAddress), Graphics::DccKeysName(resident.FilledKeys())); + throw std::runtime_error(message); + } + if (Graphics::IsDccClear(resident.FilledKeys()) && resident.FilledKeys() == keys) return false; + return Graphics::StorageImageServesKeys(resident, buffer.dccAddress); +} + +bool ResidentKeysMoved(const Graphics::StorageTexture& resident) { + const auto& own = resident.Descriptor(); + if (own.dccAddress == 0) return false; + GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes())); + return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof()) != resident.UploadedKeys(); +} + +std::optional> CompressedClearPixel(const DisplayBuffer& buffer, std::size_t bytes) { + GuestMemory::FlushGpuWrites(buffer.address, bytes); + const auto keys = DisplayKeys(buffer, bytes); + if (keys == Graphics::DccKeys::Uncompressed) return std::nullopt; + return DisplayBufferClearPixel(buffer, keys); +} + // Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory. bool NoResidentPresent() { static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr; @@ -1871,6 +1907,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (!NoResidentPresent()) { bool pending = false; resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); + if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer, bytes)) { + resident.reset(); + convert = false; + } if (!pending) { ++notPending; } else if (resident == nullptr) { @@ -1882,18 +1922,20 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { // walk: the collect memo is per worker packet, so on this thread a memoized answer could // miss a CPU write that landed after a worker's walk of the same range. GuestMemory::CollectWritesUncached(buffer.address, bytes); - if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation())) { + if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation()) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { resident->Refresh(); ++refreshedPresents; } ++residentPresents; } } + const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; auto& dumps = Dumps(); bool dumpFrame = false; if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { - const auto full = ReadDisplayBuffer(buffer); + auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); + for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; @@ -1906,7 +1948,12 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { std::fprintf(stderr, "[flip] %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); - if (!present(buffer.width, buffer.height, true, {}, &buffer, resident, filter, dumpFrame, convert)) { + VkClearColorValue uniform{}; + if (cleared) { + const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; + uniform = {{channel(2), channel(1), channel(0), channel(3)}}; + } + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; return false; @@ -1951,7 +1998,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -1965,8 +2012,9 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu const auto index = state->acquiredIndex; require(index < state->images.size() && index < state->rendered.size(), "acquired image index is out of range"); auto& rendered = state->rendered[index]; - const bool clearOnly = pixels.empty() && display == nullptr; + const bool clearOnly = pixels.empty() && display == nullptr && uniform == nullptr; require(!residentConvert || (resident != nullptr && display != nullptr), "a converted resident presentation needs its image and display buffer"); + require(uniform == nullptr || (pixels.empty() && display == nullptr && resident == nullptr), "a uniform presentation has no other source"); const bool direct = resident != nullptr && !residentConvert; // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them @@ -2024,7 +2072,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu auto pipelineBarrier = state->DeviceFunction("vkCmdPipelineBarrier"); pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (pixels.empty() && display == nullptr) { + if (clearOnly) { APS5_LOG_OUT_DEBUG("Recording swapchain clear opaque=%u image=%p", static_cast(opaque), reinterpret_cast(barrier.image)); VkClearColorValue clear{}; clear.float32[3] = opaque ? 1.0f : 0.0f; @@ -2056,7 +2104,8 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (display != nullptr) { state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat)); } - state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); + if (uniform != nullptr) state->scaler->RecordClear(commands, *uniform); + else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } VkClearColorValue letterbox{}; letterbox.float32[3] = 1.0f; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index 3efc2b3b2..f997d452d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -21,6 +21,7 @@ struct Context; enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, ClearRegister, Mixed, Unreadable }; const char* DccKeysName(DccKeys keys); +std::size_t DccKeyBytes(std::uint64_t surfaceBytes); template bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys filled, std::uint64_t namedDcc, ReadFollowed&& readFollowed, ReadNamed&& readNamed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 43d168e2e..fb98b5ada 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -374,6 +374,10 @@ const char* DccKeysName(DccKeys keys) { return "?"; } +std::size_t DccKeyBytes(std::uint64_t surfaceBytes) { + return static_cast(surfaceBytes / KeyBytes); +} + namespace { // ReadDccKeys, saying in `memoized` whether the answer came from the pending-store memo rather diff --git a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp new file mode 100644 index 000000000..b2173b7ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp @@ -0,0 +1,83 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include +#include +#include +#include +#include + +using AgcDriver::DisplayBuffer; +using AgcDriver::Graphics::DccKeys; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +template +std::string Rejection(TAction action) { + try { + action(); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +void ExpectPixels(const DisplayBuffer& buffer, DccKeys keys, std::array expected, const char* what) { + const auto pixel = AgcDriver::DisplayBufferClearPixel(buffer, keys); + for (std::size_t i = 0; i < pixel.size(); ++i) { + if (std::to_integer(pixel[i]) == expected[i]) continue; + Expect(false, std::string(what) + ": byte " + std::to_string(i) + " is " + std::to_string(std::to_integer(pixel[i])) + ", expected " + std::to_string(expected[i])); + return; + } +} + +} + +int main() { + constexpr std::uint64_t bgra = 0x8000000000000000ull; + constexpr std::uint64_t rgba = 0x8000000022000000ull; + constexpr std::uint64_t tenBit = 0x0100000000000000ull; + DisplayBuffer buffer{65536, bgra, 131, 3, 0, 0, 0x7f0000, 0x11223344}; + ExpectPixels(buffer, DccKeys::Clear0000, {0, 0, 0, 0}, "8-bit 0000"); + ExpectPixels(buffer, DccKeys::Clear0001, {0, 0, 0, 255}, "8-bit 0001"); + ExpectPixels(buffer, DccKeys::Clear1110, {255, 255, 255, 0}, "8-bit 1110"); + ExpectPixels(buffer, DccKeys::Clear1111, {255, 255, 255, 255}, "8-bit 1111"); + ExpectPixels(buffer, DccKeys::ClearRegister, {0x44, 0x33, 0x22, 0x11}, "B8G8R8A8 register clear"); + buffer.pixelFormat = rgba; + ExpectPixels(buffer, DccKeys::ClearRegister, {0x22, 0x33, 0x44, 0x11}, "R8G8B8A8 register clear"); + ExpectPixels(buffer, DccKeys::Clear0001, {0, 0, 0, 255}, "R8G8B8A8 0001"); + buffer.pixelFormat = rgba | tenBit; + ExpectPixels(buffer, DccKeys::Clear1110, {255, 255, 255, 255}, "A2B10G10R10 1110"); + ExpectPixels(buffer, DccKeys::Clear0001, {0, 0, 0, 255}, "A2B10G10R10 0001"); + buffer.dccClearColor = 0x3ff; + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 0, 255, 255}, "A2B10G10R10 register clear"); + buffer.pixelFormat = bgra | tenBit; + ExpectPixels(buffer, DccKeys::ClearRegister, {255, 0, 0, 255}, "A2R10G10B10 register clear"); + for (const auto keys : {DccKeys::Uncompressed, DccKeys::Mixed, DccKeys::Unreadable}) { + const auto message = Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, keys); }); + Expect(message.find(AgcDriver::Graphics::DccKeysName(keys)) != std::string::npos && message.find("0x7f0000") != std::string::npos, std::string("keys without a clear value were presented as one: ") + AgcDriver::Graphics::DccKeysName(keys)); + } + buffer.dccClearColor = 0x100000000ull; + Expect(Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, DccKeys::ClearRegister); }).find("register clear color") != std::string::npos, "a register clear color wider than the texel was truncated"); + buffer.dccClearColor = 0; + buffer.dccAddress = 0; + Expect(!Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, DccKeys::Clear0000); }).empty(), "a buffer without DCC metadata was presented as fast-cleared"); + buffer.dccClearColor = 1; + Expect(!Rejection([&] { AgcDriver::DisplayBufferSize(buffer); }).empty(), "a DCC clear color without metadata was accepted"); + buffer.dccClearColor = 0; + buffer.dccAddress = 0x7f0000; + buffer.tilingMode = 1; + Expect(!Rejection([&] { AgcDriver::DisplayBufferSize(buffer); }).empty(), "a linear display buffer with DCC metadata was accepted"); + buffer.tilingMode = 0; + Expect(AgcDriver::DisplayBufferSize(buffer) == 2 * 65536, "DCC metadata changed the display footprint"); + if (failures != 0) return 1; + std::puts("display buffer DCC tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index adf59924a..97c1ef47f 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -64,6 +64,7 @@ static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0; static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1; static constexpr std::uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE = 0; static constexpr std::uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8; +static constexpr std::uint32_t VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT = 0x10026c; static constexpr int VIDEO_OUT_EVENT_FLIP = 0; static constexpr int VIDEO_OUT_EVENT_VBLANK = 1; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 25fb6a241..5c8c2870e 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,8 +13,18 @@ AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, co const auto& attribute = group.attribute; if (attribute.reserved0 != 0 || attribute.pad0 != 0 || attribute.reserved1[0] != 0 || attribute.reserved1[1] != 0 || attribute.reserved1[2] != 0) throw std::runtime_error("VideoOut: reserved buffer attribute bits are set"); if (attribute.tiling_mode > 1 || (attribute.tiling_mode == 0 && attribute.pitch_in_pixel != 0) || attribute.aspect_ratio != 0 || (attribute.option != VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE && attribute.option != VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY)) throw std::runtime_error("VideoOut: unsupported tiling " + std::to_string(attribute.tiling_mode) + ", pitch " + std::to_string(attribute.pitch_in_pixel) + ", aspect ratio " + std::to_string(attribute.aspect_ratio) + " or buffer option " + std::to_string(attribute.option)); - if (group.category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED || buffer.metadataAddress != 0 || attribute.dcc_control != 0 || attribute.dcc_cb_register_clear_color != 0) throw std::runtime_error("VideoOut: DCC presentation is not implemented"); - const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel}; + const auto hex = [](std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return std::string(text); + }; + const bool compressed = group.category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; + if (!compressed && group.category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED) throw std::runtime_error("VideoOut: unknown buffer category " + std::to_string(group.category)); + if (!compressed && (buffer.metadataAddress != 0 || attribute.dcc_control != 0 || attribute.dcc_cb_register_clear_color != 0)) throw std::runtime_error("VideoOut: uncompressed buffer with DCC metadata " + hex(buffer.metadataAddress) + ", dcc_control " + hex(attribute.dcc_control) + " or DCC clear color " + hex(attribute.dcc_cb_register_clear_color)); + if (compressed && buffer.metadataAddress == 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " without DCC metadata"); + if (compressed && attribute.tiling_mode != 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " with linear tiling"); + if (compressed && (attribute.dcc_control & ~VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) != 0) throw std::runtime_error("VideoOut: unsupported DCC control " + hex(attribute.dcc_control) + " (only the block size and independence fields " + hex(VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) + " are known)"); + const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel, compressed ? buffer.metadataAddress : 0, compressed ? attribute.dcc_cb_register_clear_color : 0}; static_cast(AgcDriverDisplayBufferSize_nid_postfix(result)); return result; } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 9dd1463b0..c0835a26e 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/Shutdown.hpp" +#include #include #include #include @@ -174,6 +175,26 @@ void testDecode() { group.attribute.option = option; check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option " + std::to_string(option)) != std::string::npos, "an unsupported buffer option was accepted"); } + VideoOutBuffer dccBuffer{0, reinterpret_cast(tiled.data()), 0x7f0000}; + sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0x208, 0x11223344); + group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; + const auto compressed = DescribeVideoOutBuffer(dccBuffer, group); + check(compressed.address == dccBuffer.dataAddress && compressed.dccAddress == 0x7f0000 && compressed.dccClearColor == 0x11223344, "DCC metadata or clear color was not described"); + group.attribute.dcc_control = 0x100024; + check(DescribeVideoOutBuffer(dccBuffer, group).dccAddress == 0x7f0000, "independent 128-byte DCC blocks were rejected"); + group.attribute.dcc_control = 0x209; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("DCC control 0x209") != std::string::npos, "an unknown DCC control bit was accepted"); + group.attribute.dcc_control = 0x208; + group.attribute.tiling_mode = 1; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("linear") != std::string::npos, "a linear DCC buffer was accepted"); + group.attribute.tiling_mode = 0; + dccBuffer.metadataAddress = 0; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("without DCC metadata") != std::string::npos, "a compressed buffer without metadata was accepted"); + dccBuffer.metadataAddress = 0x7f0000; + group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("DCC metadata 0x7f0000") != std::string::npos, "an uncompressed buffer with DCC metadata was accepted"); + group.category = 2; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("category 2") != std::string::npos, "an unknown buffer category was accepted"); } void testOpenParam() { @@ -284,6 +305,36 @@ void testPresentation(bool expectUnavailable) { LibcRunShutdown_nid_postfix(); } +void testCompressedPresentation() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(6 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + fillBuffer(storage, 259, 137); + std::vector keys(storage.size() / 256); + VideoOutBuffers buffer{storage.data(), keys.data(), {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0x208, 0xff102030); + sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED, nullptr); + int64_t argument = 2000; + for (std::uint8_t key : {0x00, 0xff, 0x20, 0xc0}) { + std::fill(keys.begin(), keys.end(), key); + uint64_t target; + { + std::lock_guard lock(cfg->mutex); + target = cfg->flipStatus.count + 1; + } + sceVideoOutSubmitFlip(handle, 0, 1, ++argument); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == target; }), "DCC presentation did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.flipPendingNum == 0, "DCC flip status is wrong"); + } + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + // Flips of two buffers submitted without waiting for each other: every one completes in order // (presentations trail on the GPU, APS5_FLIP_INFLIGHT), and the shutdown retires the ones in flight. void testBackToBack() { @@ -426,6 +477,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "controls") testControls(); else if (argc == 2 && std::string(argv[1]) == "present") testPresentation(false); else if (argc == 2 && std::string(argv[1]) == "backtoback") testBackToBack(); + else if (argc == 2 && std::string(argv[1]) == "dcc") testCompressedPresentation(); else if (argc == 2 && std::string(argv[1]) == "pacing") testReleaseVblank(); else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testPresentation(true); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..2f8f73322 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,6 +29,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted +- [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order @@ -116,6 +117,7 @@ Throughout the project, every function at every stage either **does exactly what - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. +- [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. From c0c34cc83c6eb92dc654c2d171868374b83cfc2a Mon Sep 17 00:00:00 2001 From: raghav314 Date: Mon, 5 Oct 2026 19:52:21 +0530 Subject: [PATCH 30/43] feat(ajm): implement sceAjmBatchCancel sceAjmBatchStart processes every job before it returns the batch id, so a batch that has not been waited on is never pending: cancelling it returns 0. A batch that was never issued or was already waited on returns INVALID_BATCH, and a context id that was never issued returns INVALID_CONTEXT, as in shadPS4. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/Export.cpp | 7 ------- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 7 +++++++ core/libs/tests/GuestAjm.cpp | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 7fe1d9502..62c0bc44e 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -14,13 +14,6 @@ static void AjmStub(const char* name) { extern "C" { -int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { - (void)context; - (void)batch; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 3b30d8d53..181ffbb05 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -26,6 +26,7 @@ extern "C" { // memory follow the SDK layouts. namespace { +constexpr int SCE_AJM_ERROR_INVALID_CONTEXT = static_cast(0x80930002); constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast(0x80930003); constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast(0x80930004); constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); @@ -1009,6 +1010,12 @@ int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout return 0; } +int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; + std::lock_guard lock(g_batchLock); + return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH; +} + int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { (void)info; if (error) std::memset(error, 0, sizeof(*error)); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 71524e885..e832b1b16 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -25,6 +25,7 @@ int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void* int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo*, std::uint32_t, void*, std::size_t); int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); +int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -339,6 +340,25 @@ void TestBatchWaitRelease(std::uint32_t context) { Require(sceAjmBatchWait(context, second + 1, 0, &error) == invalidBatch); } +void TestBatchCancel(std::uint32_t context) { + constexpr int invalidContext = static_cast(0x80930002); + constexpr int invalidBatch = static_cast(0x80930004); + std::vector batch(64); + AjmBatchInfo info{}; + AjmBatchError error{}; + std::uint32_t id = 0; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0); + Require(sceAjmBatchCancel(context, id) == 0); + Require(sceAjmBatchCancel(context, id) == 0); + Require(sceAjmBatchCancel(0, id) == invalidContext); + Require(sceAjmBatchCancel(context + 1, id) == invalidContext); + Require(sceAjmBatchWait(context, id, 0, &error) == 0); + Require(sceAjmBatchCancel(context, id) == invalidBatch); + Require(sceAjmBatchCancel(context, 0) == invalidBatch); + Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); +} + } int main() { @@ -362,5 +382,6 @@ int main() { TestGaplessDecode(context); TestCodecInfo(context); TestBatchWaitRelease(context); + TestBatchCancel(context); Require(sceAjmFinalize(context) == 0); } From fc96f96ac5e1213dffc41cff9cec65720a3042c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 00:25:58 +0200 Subject: [PATCH 31/43] feat(ajm): parse the original file length in sceAjmDecMp3ParseFrame Co-Authored-By: Claude --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 156 +++++++++++++++++---- core/libs/tests/GuestAjm.cpp | 106 ++++++++++++++ docs/dev/TechnicalDebt.md | 4 +- 3 files changed, 237 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 3b30d8d53..45c461a9e 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -492,6 +492,134 @@ bool ParseMp3Frame(const std::uint8_t* data, std::size_t size, Mp3Frame& frame) return true; } +constexpr std::uint32_t MP3_OFL_NONE = 0; +constexpr std::uint32_t MP3_OFL_LAME = 1; +constexpr std::uint32_t MP3_OFL_VBRI = 2; +constexpr std::uint32_t MP3_OFL_FGH = 3; +constexpr std::uint32_t MP3_OFL_VBRI_AND_FGH = 4; + +std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) { + std::uint32_t value = 0; + for (std::size_t i = 0; i < bytes; ++i) value = (value << 8u) | data[i]; + return value; +} + +int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame); + +void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) { + const std::size_t end = std::min(streamSize, frame->frame_size); + if (end <= 4) return; + const std::size_t available = end - 4; + std::size_t bit = 0; + const auto readBits = [&](std::uint32_t count) { + std::uint32_t value = 0; + for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u); + return value; + }; + const bool mpeg1 = ((header >> 19u) & 3u) == 3u; + const bool mono = ((header >> 6u) & 3u) == 3u; + bit += mpeg1 ? (mono ? 18 : 20) : (mono ? 9 : 10); + std::uint32_t mainDataBits = 0; + for (std::uint32_t granule = 0; granule < (mpeg1 ? 2u : 1u); ++granule) { + for (std::uint32_t channel = 0; channel < frame->num_channels; ++channel) { + mainDataBits += readBits(12); + bit += mpeg1 ? 47 : 51; + } + } + bit += mainDataBits; + std::size_t at = 4 + std::min((bit + 7) / 8, available); + + if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) { + const std::uint8_t flags = stream[at + 7]; + std::size_t field = at + 8; + std::uint32_t frames = 0; + if (flags & 0x01u) { + if (field + 4 > end) return; + frames = ReadBigEndian(stream + field, 4); + frame->num_frames = frames; + field += 4; + } + if (flags & 0x02u) { + field += 4; + if (field > end) return; + } + if (flags & 0x04u) { + field += 100; + if (field > end) return; + } + if (flags & 0x08u) { + field += 4; + if (field > end) return; + } + if (field + 0x18 <= end && std::memcmp(stream + field, "LAME", 4) == 0) { + const std::uint32_t delay = (std::uint32_t{stream[field + 0x15]} << 4u) | (stream[field + 0x16] >> 4u); + const std::uint32_t padding = ((std::uint32_t{stream[field + 0x16]} & 0x0Fu) << 8u) | stream[field + 0x17]; + if ((flags & 0x01u) && frames > 0) frame->total_samples = frames * frame->samples_per_channel - (delay + padding); + frame->encoder_delay = frame->samples_per_channel + delay + 529; + frame->ofl_type = MP3_OFL_LAME; + } + } else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) { + NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)"); + } else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) { + frame->encoder_delay = ReadBigEndian(stream + at + 6, 2); + frame->ofl_type = MP3_OFL_VBRI; + if (frame->frame_size <= streamSize) { + AjmDecMp3ParseFrame next{}; + if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { + frame->encoder_delay += next.encoder_delay; + frame->total_samples = next.total_samples; + frame->ofl_type = MP3_OFL_VBRI_AND_FGH; + } + } + } else if (at + 10 <= end) { + while (at + 9 < end && stream[at] != 0xB4u) ++at; + if (at + 9 >= end) return; + std::uint8_t crc = 0xFF; + for (std::size_t i = 0; i < 9; ++i) { + for (int shift = 7; shift >= 0; --shift) { + const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); + crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); + } + } + if (stream[at + 9] != crc) return; + frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); + frame->total_samples = ReadBigEndian(stream + at + 3, 4); + frame->ofl_type = MP3_OFL_FGH; + } +} + +int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { + if (!stream || streamSize < 4 || !frame) return SCE_AJM_ERROR_INVALID_PARAMETER; + static constexpr std::uint32_t sampleRates[4][4] = {{11025, 12000, 8000, 0}, {0, 0, 0, 0}, {22050, 24000, 16000, 0}, {44100, 48000, 32000, 0}}; + static constexpr std::uint32_t kbps[4][16] = { + {0, 8, 16, 24, 32, 40, 48, 56, 64, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}, + {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}, + }; + const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; + const std::uint32_t version = (header >> 19u) & 3u; + const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; + const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; + const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; + AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); + if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)"); + if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)"); + const bool mpeg1 = version == 3; + frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); + frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; + frame->samples_per_channel = mpeg1 ? 1152u : 576u; + frame->bitrate = bitrate; + frame->sample_rate = sampleRate; + frame->encoder_delay = 0; + frame->num_frames = 0; + frame->total_samples = 0; + frame->ofl_type = MP3_OFL_NONE; + if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame); + return 0; +} + void OpenMp3(Instance& instance) { static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); (void)quiet; @@ -870,33 +998,7 @@ int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9Conf } int APS5_VABI sceAjmDecMp3ParseFrame(const uint8_t* stream, uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { - if (!stream || streamSize < 4 || !frame) return SCE_AJM_ERROR_INVALID_PARAMETER; - static constexpr std::uint32_t sampleRates[4][4] = {{11025, 12000, 8000, 0}, {0, 0, 0, 0}, {22050, 24000, 16000, 0}, {44100, 48000, 32000, 0}}; - static constexpr std::uint32_t kbps[4][16] = { - {0, 8, 16, 24, 32, 40, 48, 56, 64, 0, 0, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}, - {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}, - }; - const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; - const std::uint32_t version = (header >> 19u) & 3u; - const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; - const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; - const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; - AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); - if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; - if (parseOfl) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup)"); - const bool mpeg1 = version == 3; - frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); - frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; - frame->samples_per_channel = mpeg1 ? 1152u : 576u; - frame->bitrate = bitrate; - frame->sample_rate = sampleRate; - frame->encoder_delay = 0; - frame->num_frames = 0; - frame->total_samples = 0; - frame->ofl_type = 0; - return 0; + return ParseMp3Header(stream, streamSize, parseOfl, frame); } int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) { diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 71524e885..12b8acd63 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -201,6 +201,111 @@ void TestMp3ParseFrame() { Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); } +const std::uint8_t LAME_MPEG1_STEREO_VBR[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x69, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x09, 0x28, 0x00, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x9F, 0x9F, 0x9F, 0x9F, + 0x9F, 0x9F, 0x9F, 0x9F, 0x9F, 0x9F, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, + 0xBE, 0xBE, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x0A, 0x4C, 0x41, 0x4D, 0x45, 0x34, 0x2E, 0x30, 0x20, 0x00, 0x04, 0x48, 0x00, + 0x00, 0x00, 0x00, 0x2E, 0x1A, 0x00, 0x00, 0x15, 0x20, 0x24, 0x03, 0xB0, 0x45, 0x00, 0x01, 0x9A, 0x00, 0x00, 0x09, 0x28, 0x06, 0x5C, 0x53, 0xC5, +}; + +const std::uint8_t LAME_MPEG1_MONO_CBR[] = { + 0xFF, 0xFB, 0x98, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6E, 0x66, + 0x6F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xE6, 0xE6, 0xE6, + 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x38, 0x4C, 0x41, 0x4D, + 0x45, 0x34, 0x2E, 0x30, 0x20, 0x00, 0x01, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x15, 0x00, 0x00, 0x14, 0x80, 0x24, 0x03, 0xB0, 0x22, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x18, 0xC0, 0x9A, 0x66, 0xF0, 0x95, +}; + +const std::uint8_t LAME_MPEG2_MONO_CBR[] = { + 0xFF, 0xF3, 0x80, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6E, 0x66, 0x6F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x11, 0x23, 0x00, 0x0C, 0x0C, 0x0C, 0x0C, 0x19, 0x19, 0x19, 0x19, 0x19, 0x26, 0x26, 0x26, 0x26, 0x26, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x40, 0x40, 0x40, 0x40, 0x40, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x59, 0x59, 0x59, 0x59, 0x59, 0x66, 0x66, 0x66, 0x66, 0x66, 0x73, 0x73, 0x73, + 0x73, 0x73, 0x80, 0x80, 0x80, 0x80, 0x80, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0x99, 0x99, 0x99, 0x99, 0x99, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xB3, 0xB3, + 0xB3, 0xB3, 0xB3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xF3, + 0xF3, 0xF3, 0xF3, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x38, 0x4C, 0x41, 0x4D, 0x45, 0x34, 0x2E, 0x30, 0x20, 0x00, 0x01, 0x6E, + 0x00, 0x00, 0x00, 0x00, 0x2E, 0x1E, 0x00, 0x00, 0x14, 0x40, 0x24, 0x03, 0xB0, 0x22, 0x00, 0x00, 0x40, 0x00, 0x00, 0x11, 0x23, 0xF6, 0xE8, 0xE2, + 0xEB, +}; + +const std::uint8_t FGH_FRAME[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x04, 0x51, 0x00, 0x06, 0xBA, 0xA8, 0x00, 0x00, 0xA0, +}; + +const std::uint8_t VBRI_FRAME[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x42, 0x52, 0x49, 0x00, 0x01, 0x02, 0x40, 0x00, 0x4B, 0x00, 0x00, + 0x10, 0x4A, 0x00, 0x00, 0x00, 0x0A, +}; + +const std::uint8_t FGH_BAD_CRC_FRAME[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x04, 0x51, 0x00, 0x06, 0xBA, 0xA8, 0x00, 0x00, 0xA1, +}; + +std::vector Mp3Frame(const std::uint8_t* prefix, std::size_t prefixSize, std::size_t frameSize) { + std::vector frame(frameSize, 0); + std::memcpy(frame.data(), prefix, prefixSize); + return frame; +} + +bool OflParsesTo(const std::vector& stream, std::uint32_t streamSize, std::uint32_t frames, std::uint32_t delay, std::uint32_t total, std::uint32_t type) { + AjmDecMp3ParseFrame frame; + std::memset(&frame, 0xFF, sizeof(frame)); + if (sceAjmDecMp3ParseFrame(stream.data(), streamSize, 1, &frame) != 0) return false; + return frame.num_frames == frames && frame.encoder_delay == delay && frame.total_samples == total && frame.ofl_type == type; +} + +bool OflThrows(const std::vector& stream) { + AjmDecMp3ParseFrame frame{}; + try { + sceAjmDecMp3ParseFrame(stream.data(), static_cast(stream.size()), 1, &frame); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void TestMp3ParseOfl() { + const auto stereo = Mp3Frame(LAME_MPEG1_STEREO_VBR, sizeof(LAME_MPEG1_STEREO_VBR), 417); + Require(OflParsesTo(stereo, 417, 10, 1152 + 576 + 529, 10000, 1)); + Require(ParsesTo(stereo.data(), 417, 417, 2, 1152, 128000, 44100)); + Require(OflParsesTo(stereo, 48, 10, 0, 0, 0)); + Require(OflParsesTo(stereo, 46, 0, 0, 0, 0)); + const auto mono = Mp3Frame(LAME_MPEG1_MONO_CBR, sizeof(LAME_MPEG1_MONO_CBR), 576); + Require(OflParsesTo(mono, 576, 10, 1152 + 576 + 529, 10000, 1)); + const auto mpeg2 = Mp3Frame(LAME_MPEG2_MONO_CBR, sizeof(LAME_MPEG2_MONO_CBR), 208); + Require(OflParsesTo(mpeg2, 208, 20, 576 + 576 + 529, 10000, 1)); + + const auto fgh = Mp3Frame(FGH_FRAME, sizeof(FGH_FRAME), 417); + Require(OflParsesTo(fgh, 417, 0, 1105, 441000, 3)); + Require(OflParsesTo(Mp3Frame(FGH_BAD_CRC_FRAME, sizeof(FGH_BAD_CRC_FRAME), 417), 417, 0, 0, 0, 0)); + auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + Require(OflParsesTo(vbri, 417, 0, 576, 0, 2)); + vbri.insert(vbri.end(), fgh.begin(), fgh.end()); + Require(OflParsesTo(vbri, 834, 0, 576 + 1105, 441000, 4)); + Require(OflParsesTo(vbri, 480, 0, 576, 0, 2)); + + auto protectedFrame = stereo; + protectedFrame[1] = 0xFA; + Require(OflThrows(protectedFrame)); + auto layer2 = stereo; + layer2[1] = 0xFD; + Require(OflThrows(layer2)); + auto monoVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + monoVbri[3] = 0xC4; + Require(OflThrows(monoVbri)); +} + struct GaplessDecode { std::uint32_t totalSamples; std::uint16_t skipSamples; @@ -356,6 +461,7 @@ int main() { Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter); Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter); TestMp3ParseFrame(); + TestMp3ParseOfl(); TestMp3(context); TestOpus(context); TestDecodeSingle(context); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..4527d7d88 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -75,7 +75,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture -- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps +- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature @@ -121,7 +121,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. -- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero): reading it from the frame's Xing/Info, VBRI or Fraunhofer data is not implemented. +- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. From 06d2ae5f05534cb73bbfafff286301a260a47556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:54:11 +0200 Subject: [PATCH 32/43] feat(audio3d): implement the port queue Initialize, PortOpen/Close, PortSetAttribute, PortGetQueueLevel, PortAdvance and PortPush. One port, validation order and error codes from the PS5 module as read by prosper; blocking push paced at granularity samples per frame at 48 kHz like KytyPS5. Co-Authored-By: Claude --- core/libs/prx/libSceAudio3d/CMakeLists.txt | 9 + core/libs/prx/libSceAudio3d/Export.cpp | 167 ++++++++++++++---- core/libs/tests/GuestAudio3dPort.cpp | 187 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 + 4 files changed, 336 insertions(+), 29 deletions(-) create mode 100644 core/libs/tests/GuestAudio3dPort.cpp diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index c324ba678..e93a55d05 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -18,4 +18,13 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_parameters PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_port_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dPort.cpp) + target_include_directories(guest_audio3d_port_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio3d_port_tests PRIVATE libSceAudio3d libc) + configure_windows_unwind(guest_audio3d_port_tests) + add_test(NAME guest_audio3d_port COMMAND guest_audio3d_port_tests) + if(WIN32) + set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index d82269005..7a0371a52 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,9 +1,62 @@ +#include #include #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); +constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); +constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); +constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); + +constexpr int AUDIO3D_USER_ID_SYSTEM = 0xFF; +constexpr std::uint32_t AUDIO3D_PORT_ID = 0; +constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; +constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; +constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; +constexpr std::uint32_t AUDIO3D_BLOCKING_ASYNC = 0; +constexpr std::uint32_t AUDIO3D_BLOCKING_SYNC = 1; +constexpr std::uint32_t AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001; +constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002; +constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003; + +using Clock = std::chrono::steady_clock; + +struct Audio3dPort { + bool open = false; + std::uint32_t queue_depth = 0; + Clock::duration frame_duration{}; + std::uint32_t advanced = 0; + std::deque playing; +}; + +std::mutex g_mutex; +bool g_initialized = false; +Audio3dPort g_port; + +[[noreturn]] void Unsupported(const char* function, const std::string& what) { + throw std::runtime_error(std::string(function) + ": " + what); +} + +bool PortIsOpen(std::uint32_t port_id) { + return port_id == AUDIO3D_PORT_ID && g_port.open; +} + +std::uint32_t QueueLevel(Clock::time_point now) { + while (!g_port.playing.empty() && g_port.playing.front() <= now) g_port.playing.pop_front(); + return g_port.advanced + static_cast(g_port.playing.size()); +} + +} + extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { @@ -14,53 +67,109 @@ void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { } int APS5_VABI sceAudio3dInitialize(int64_t reserved) { - (void)reserved; - NotImplemented_nid_no_patch(__func__); - return 0; + if (reserved != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + std::lock_guard lock(g_mutex); + if (g_initialized) Unsupported(__func__, "repeated initialization"); + g_initialized = true; + return 0; } int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (QueueLevel(Clock::now()) >= g_port.queue_depth) Unsupported(__func__, "queue is full"); + ++g_port.advanced; + return 0; } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + g_port = Audio3dPort{}; + return 0; } int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_level, uint32_t* queue_available) { - (void)port_id; - (void)queue_level; - (void)queue_available; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (queue_level == nullptr && queue_available == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::uint32_t level = QueueLevel(Clock::now()); + if (queue_level != nullptr) *queue_level = level; + if (queue_available != nullptr) *queue_available = g_port.queue_depth - level; + return 0; } int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, uint32_t* id) { - (void)user_id; - (void)parameters; - (void)id; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_initialized) return AUDIO3D_ERROR_NOT_READY; + if (user_id != AUDIO3D_USER_ID_SYSTEM || parameters == nullptr || id == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + std::uint32_t num_beds = 2; + switch (parameters->size_this) { + case 0x10: + case 0x18: + Unsupported(__func__, "parameter size " + std::to_string(parameters->size_this) + " selects an unsupported buffer mode"); + case 0x20: + break; + case 0x28: + num_beds = parameters->num_beds; + break; + default: + return AUDIO3D_ERROR_INVALID_PARAMETER; + } + if (parameters->rate != AUDIO3D_RATE_48000) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->granularity < 0x100 || (parameters->granularity & 0xFF) != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->max_objects == 0 || parameters->queue_depth == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->buffer_mode > AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (num_beds != 2 && num_beds != 3) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) { + Unsupported(__func__, "buffer mode " + std::to_string(parameters->buffer_mode)); + } + if (num_beds != 2) Unsupported(__func__, "3 beds"); + if (g_port.open) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + g_port = Audio3dPort{}; + g_port.open = true; + g_port.queue_depth = parameters->queue_depth; + g_port.frame_duration = std::chrono::duration_cast( + std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); + *id = AUDIO3D_PORT_ID; + return 0; } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - (void)port_id; - (void)blocking; - NotImplemented_nid_no_patch(__func__); - return 0; + Clock::time_point wait_until; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { + Unsupported(__func__, "blocking mode " + std::to_string(blocking)); + } + const Clock::time_point now = Clock::now(); + const std::uint32_t level = QueueLevel(now); + Clock::time_point end = g_port.playing.empty() ? now : g_port.playing.back(); + for (; g_port.advanced != 0; --g_port.advanced) { + end += g_port.frame_duration; + g_port.playing.push_back(end); + } + if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; + wait_until = g_port.playing[level - g_port.queue_depth]; + } + std::this_thread::sleep_until(wait_until); + return 0; } int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id, const void* attribute, size_t attribute_size) { - (void)port_id; - (void)attribute_id; - (void)attribute; - (void)attribute_size; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (attribute == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (attribute_size != 4) Unsupported(__func__, "attribute size " + std::to_string(attribute_size)); + switch (attribute_id) { + case AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL: + case AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS: + case AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE: + return 0; + default: + Unsupported(__func__, "attribute " + std::to_string(attribute_id)); + } } } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp new file mode 100644 index 000000000..c0e58ad57 --- /dev/null +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -0,0 +1,187 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); +int APS5_VABI sceAudio3dInitialize(std::int64_t reserved); +int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, std::uint32_t* id); +int APS5_VABI sceAudio3dPortClose(std::uint32_t port_id); +int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t attribute_id, const void* attribute, std::size_t attribute_size); +int APS5_VABI sceAudio3dPortGetQueueLevel(std::uint32_t port_id, std::uint32_t* queue_level, std::uint32_t* queue_available); +int APS5_VABI sceAudio3dPortAdvance(std::uint32_t port_id); +int APS5_VABI sceAudio3dPortPush(std::uint32_t port_id, std::uint32_t blocking); +} + +namespace { + +constexpr int INVALID_PORT = static_cast(0x80EA0002); +constexpr int INVALID_PARAMETER = static_cast(0x80EA0004); +constexpr int OUT_OF_RESOURCES = static_cast(0x80EA0006); +constexpr int NOT_READY = static_cast(0x80EA0007); +constexpr int SYSTEM_USER = 0xFF; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +void RequireThrows(F f, const char* message) { + bool threw = false; + try { + f(); + } catch (const std::exception&) { + threw = true; + } + Require(threw, message); +} + +Audio3dOpenParameters Defaults() { + Audio3dOpenParameters parameters{}; + sceAudio3dGetDefaultOpenParameters(¶meters); + return parameters; +} + +int Open(const Audio3dOpenParameters& parameters, std::uint32_t* id) { + return sceAudio3dPortOpen(SYSTEM_USER, ¶meters, id); +} + +void RequireLevel(std::uint32_t level, std::uint32_t available, const char* message) { + std::uint32_t got_level = 99; + std::uint32_t got_available = 99; + Require(sceAudio3dPortGetQueueLevel(0, &got_level, &got_available) == 0, message); + Require(got_level == level && got_available == available, message); +} + +void CheckBeforeInitialize() { + std::uint32_t id = 7; + Audio3dOpenParameters parameters = Defaults(); + Require(sceAudio3dInitialize(1) == INVALID_PARAMETER, "non-zero reserved must be rejected"); + Require(Open(parameters, &id) == NOT_READY, "open before initialize must be NOT_READY"); + Require(id == 7, "a failed open must not write the port id"); + Require(sceAudio3dPortOpen(1, nullptr, nullptr) == NOT_READY, "NOT_READY is checked before the arguments"); + std::uint32_t level = 0; + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == INVALID_PORT, "queue level needs an open port"); + Require(sceAudio3dPortClose(0) == INVALID_PORT, "close needs an open port"); + Require(sceAudio3dPortAdvance(0) == INVALID_PORT, "advance needs an open port"); + Require(sceAudio3dPortPush(0, 0) == INVALID_PORT, "push needs an open port"); + float value = 0.0f; + Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, sizeof(value)) == INVALID_PORT, "set attribute needs an open port"); + Require(sceAudio3dInitialize(0) == 0, "initialize"); + RequireThrows([] { sceAudio3dInitialize(0); }, "repeated initialize must throw"); +} + +void CheckOpenParameters() { + std::uint32_t id = 7; + const Audio3dOpenParameters defaults = Defaults(); + Require(sceAudio3dPortOpen(1, &defaults, &id) == INVALID_PARAMETER, "user id must be the system user"); + Require(sceAudio3dPortOpen(SYSTEM_USER, nullptr, &id) == INVALID_PARAMETER, "null parameters"); + Require(sceAudio3dPortOpen(SYSTEM_USER, &defaults, nullptr) == INVALID_PARAMETER, "null port id"); + auto rejects = [&](auto change, const char* message) { + Audio3dOpenParameters parameters = defaults; + change(parameters); + Require(Open(parameters, &id) == INVALID_PARAMETER, message); + Require(id == 7, "a failed open must not write the port id"); + }; + rejects([](auto& p) { p.size_this = 0x30; }, "unknown parameter size"); + rejects([](auto& p) { p.size_this = 0x21; }, "parameter size is not masked"); + rejects([](auto& p) { p.rate = 1; }, "only 48 kHz"); + rejects([](auto& p) { p.granularity = 0x80; }, "granularity below 256"); + rejects([](auto& p) { p.granularity = 0x180; }, "granularity not a multiple of 256"); + rejects([](auto& p) { p.max_objects = 0; }, "zero objects"); + rejects([](auto& p) { p.queue_depth = 0; }, "zero queue depth"); + rejects([](auto& p) { p.buffer_mode = 3; }, "buffer mode above 2"); + rejects([](auto& p) { p.size_this = 0x28; p.num_beds = 4; }, "4 beds"); + rejects([](auto& p) { p.size_this = 0x28; p.num_beds = 1; }, "1 bed"); + auto throws = [&](auto change, const char* message) { + Audio3dOpenParameters parameters = defaults; + change(parameters); + RequireThrows([&] { Open(parameters, &id); }, message); + }; + throws([](auto& p) { p.size_this = 0x10; }, "0x10 parameters select buffer mode 0"); + throws([](auto& p) { p.size_this = 0x18; }, "0x18 parameters select buffer mode 1"); + throws([](auto& p) { p.buffer_mode = 1; }, "buffer mode 1"); + throws([](auto& p) { p.size_this = 0x28; p.num_beds = 3; }, "3 beds"); +} + +void CheckOpenClose() { + std::uint32_t id = 7; + Audio3dOpenParameters parameters = Defaults(); + parameters.size_this = 0x28; + parameters.num_beds = 2; + Require(Open(parameters, &id) == 0 && id == 0, "open hands out port 0"); + std::uint32_t second = 7; + Require(Open(parameters, &second) == OUT_OF_RESOURCES && second == 7, "only one port"); + std::uint32_t level = 0; + Require(sceAudio3dPortGetQueueLevel(1, &level, nullptr) == INVALID_PORT, "port 1 does not exist"); + Require(sceAudio3dPortClose(1) == INVALID_PORT, "close of port 1"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(sceAudio3dPortClose(0) == INVALID_PORT, "double close"); + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == INVALID_PORT, "closed port"); +} + +void CheckAttributes() { + float value = 0.5f; + int flag = 1; + Require(sceAudio3dPortSetAttribute(0, 0x10001, nullptr, 4) == INVALID_PARAMETER, "null attribute"); + Require(sceAudio3dPortSetAttribute(1, 0x10001, &value, 4) == INVALID_PORT, "attribute on port 1"); + Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, 4) == 0, "late reverb level"); + Require(sceAudio3dPortSetAttribute(0, 0x10002, &value, 4) == 0, "downmix spread radius"); + Require(sceAudio3dPortSetAttribute(0, 0x10003, &flag, 4) == 0, "downmix spread height aware"); + RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10004, &value, 4); }, "unknown attribute must throw"); + RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10001, &value, 8); }, "wrong attribute size must throw"); +} + +void CheckQueue() { + using namespace std::chrono; + constexpr std::uint32_t granularity = 0x1800; + constexpr auto frame = microseconds(1000000ull * granularity / 48000); + Audio3dOpenParameters parameters = Defaults(); + parameters.granularity = granularity; + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + CheckAttributes(); + Require(sceAudio3dPortGetQueueLevel(0, nullptr, nullptr) == INVALID_PARAMETER, "both outputs null"); + std::uint32_t available = 0; + Require(sceAudio3dPortGetQueueLevel(0, nullptr, &available) == 0 && available == 2, "level pointer is optional"); + RequireLevel(0, 2, "empty queue"); + Require(sceAudio3dPortPush(0, 1) == 0, "push of an empty queue"); + RequireLevel(0, 2, "empty push queues nothing"); + Require(sceAudio3dPortAdvance(0) == 0, "advance 1"); + RequireLevel(1, 1, "one frame queued"); + Require(sceAudio3dPortAdvance(0) == 0, "advance 2"); + RequireLevel(2, 0, "queue full"); + RequireThrows([] { sceAudio3dPortAdvance(0); }, "advance into a full queue must throw"); + RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); + + auto start = steady_clock::now(); + Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); + auto waited = steady_clock::now() - start; + Require(waited >= frame - milliseconds(2), "blocking push waits until one frame has played"); + RequireLevel(1, 1, "blocking push returns with one free slot"); + + Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); + start = steady_clock::now(); + Require(sceAudio3dPortPush(0, 0) == 0, "async push"); + Require(steady_clock::now() - start < frame, "async push does not wait for a frame"); + std::uint32_t level = 0; + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level >= 1, "async push keeps frames queued"); + std::this_thread::sleep_for(frame * 3); + RequireLevel(0, 2, "frames drain at the port's rate"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +} + +int main() { + CheckBeforeInitialize(); + CheckOpenParameters(); + CheckOpenClose(); + CheckQueue(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..80b700346 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -20,6 +20,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). +- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. @@ -31,6 +32,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware +- [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale From 42869efbfa427a8d9b7c20f1118d40da2232972f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 00:28:18 +0200 Subject: [PATCH 33/43] fix(audio3d): wait until the slot frees in blocking push --- core/libs/prx/libSceAudio3d/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 7a0371a52..705f81119 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -153,7 +153,7 @@ int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; wait_until = g_port.playing[level - g_port.queue_depth]; } - std::this_thread::sleep_until(wait_until); + while (Clock::now() < wait_until) std::this_thread::sleep_until(wait_until); return 0; } From 948d3cd047731cdf3a00733f1a606ba1d6294a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:22:34 +0200 Subject: [PATCH 34/43] feat(libc): implement std::_Execute_once Co-Authored-By: Claude --- core/libs/prx/libc/CMakeLists.txt | 10 ++++ core/libs/prx/libc/Export.cpp | 6 -- core/libs/prx/libc/src/CxxAbiSupport.cpp | 12 ++++ core/libs/tests/GuestExecuteOnce.cpp | 70 ++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 core/libs/tests/GuestExecuteOnce.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index f5736578e..847b8ffc3 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -77,6 +77,16 @@ if(BUILD_TESTING) set_tests_properties(guest_math PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnce.cpp) + target_include_directories(guest_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_execute_once_tests PRIVATE libc) + configure_windows_unwind(guest_execute_once_tests) + add_test(NAME guest_execute_once COMMAND guest_execute_once_tests) + set_tests_properties(guest_execute_once PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_execute_once PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_working_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestWorkingDirectory.cpp) target_include_directories(guest_working_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_working_directory_tests PRIVATE libc) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index a9687082d..bc7e45119 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -64,12 +64,6 @@ int APS5_VABI libcCyberUnknown19(void) { } -APS5_EXPORT("DiGVep5yB5w", libcCyberUnknown01); -std::uint64_t APS5_VABI libcCyberUnknown01(void) { - NotImplemented_nid_no_patch("DiGVep5yB5w"); - return 0; -} - APS5_EXPORT("Ye20uNnlglA", libcCyberUnknown02); std::uint64_t APS5_VABI libcCyberUnknown02(void) { NotImplemented_nid_no_patch("Ye20uNnlglA"); diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index b3e467664..8f32a8572 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "prx/libc/include/General.hpp" @@ -40,4 +41,15 @@ int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (*destructor)(void const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std::iostream_category(); } +int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix( + int* flag, int (APS5_VABI *callback)(void*, void*, void**), void* arg +) { + static std::recursive_mutex mutex; + std::lock_guard lock(mutex); + if (*flag != 0) return 1; + if (callback(nullptr, arg, nullptr) == 0) return 0; + *flag = 1; + return 1; +} + } diff --git a/core/libs/tests/GuestExecuteOnce.cpp b/core/libs/tests/GuestExecuteOnce.cpp new file mode 100644 index 000000000..327fbe8c3 --- /dev/null +++ b/core/libs/tests/GuestExecuteOnce.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +using Callback = int (APS5_VABI *)(void*, void*, void**); +extern "C" int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(int*, Callback, void*); +static int ExecuteOnce(int* flag, Callback callback, void* arg) { + return _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(flag, callback, arg); +} +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}, arrived{0}; +static int control = 0, nested = 0, failing = 0, throwing = 0; +static int published = 0, nestedValue = 0, failures = 0, throws = 0; +static void* seenFirst = &control; +static void* seenArg = nullptr; +static void** seenThird = reinterpret_cast(&control); + +static int APS5_VABI Nested(void*, void* arg, void**) { + nestedValue = *static_cast(arg); + return 1; +} +static int APS5_VABI Initialize(void* first, void* arg, void** third) { + ++calls; + seenFirst = first; + seenArg = arg; + seenThird = third; + while (arrived.load() != 16) std::this_thread::yield(); + int value = 17; + Require(ExecuteOnce(&nested, Nested, &value) == 1); + published = 42; + return 5; +} +static int APS5_VABI Fail(void*, void*, void**) { + ++failures; + return 0; +} +static int APS5_VABI Throw(void*, void*, void**) { + if (++throws == 1) throw 7; + return 1; +} +static int APS5_VABI Unreachable(void*, void*, void**) { + std::abort(); +} + +int main() { + int token = 0; + std::array workers; + for (auto& worker : workers) worker = std::thread([&token] { + ++arrived; + Require(ExecuteOnce(&control, Initialize, &token) == 1); + Require(published == 42 && nestedValue == 17); + }); + for (auto& worker : workers) worker.join(); + Require(calls == 1 && control == 1 && nested == 1); + Require(seenFirst == nullptr && seenArg == &token && seenThird == nullptr); + Require(ExecuteOnce(&control, Unreachable, nullptr) == 1 && calls == 1); + + Require(ExecuteOnce(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 1); + Require(ExecuteOnce(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 2); + + try { ExecuteOnce(&throwing, Throw, nullptr); std::abort(); } + catch (int value) { Require(value == 7 && throwing == 0); } + Require(ExecuteOnce(&throwing, Throw, nullptr) == 1 && throwing == 1 && throws == 2); + + int preset = -3; + Require(ExecuteOnce(&preset, Unreachable, nullptr) == 1 && preset == -3); +} From f4b5b1ba508d52e5c8cc9c9a58969ba245f857c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:38:06 +0200 Subject: [PATCH 35/43] feat(libSceShare): capture, open menu and current status calls Co-Authored-By: Claude --- core/libs/prx/libSceShare/Export.cpp | 34 ++++++++++++++++------------ core/libs/tests/GuestShare.cpp | 33 +++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index caabed9b9..eb65ee977 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -7,6 +8,7 @@ namespace { +constexpr std::int32_t ERROR_INVALID_PARAM = static_cast(0x81960002); constexpr std::int32_t ERROR_NOT_SUPPORTED = static_cast(0x81960007); constexpr std::int32_t REQUEST_ID_INVALID = -1; @@ -15,17 +17,19 @@ constexpr std::int32_t REQUEST_ID_INVALID = -1; extern "C" { int APS5_VABI sceShareCaptureScreenshot(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)param; + if (req_id != nullptr) { + *req_id = REQUEST_ID_INVALID; + } + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareCaptureVideoClip(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)param; + if (req_id != nullptr) { + *req_id = REQUEST_ID_INVALID; + } + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareFeaturePermit(uint32_t feature_flags) { @@ -39,10 +43,11 @@ int APS5_VABI sceShareFeatureProhibit(uint32_t feature_flags) { } int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; + if (feature_flag == 0 || status == nullptr) { + return ERROR_INVALID_PARAM; + } + std::memset(status, 0, sizeof(*status)); + return 0; } int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { @@ -53,9 +58,8 @@ int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t } int APS5_VABI sceShareOpenMenuForContent(const void* content_id) { - (void)content_id; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)content_id; + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareRegisterContentEventCallback(void* callback, void* user_data) { diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 353b80bb4..f45247047 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -1,9 +1,15 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include extern "C" { int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, std::int32_t* req_id); +int APS5_VABI sceShareCaptureScreenshot(const void* param, std::int32_t* req_id); +int APS5_VABI sceShareCaptureVideoClip(const void* param, std::int32_t* req_id); +int APS5_VABI sceShareGetCurrentStatus(std::uint32_t feature_flag, void* status); +int APS5_VABI sceShareOpenMenuForContent(const void* content_id); } namespace { @@ -26,5 +32,32 @@ int main() { Require(sceShareCaptureScreenshotExtended(param, nullptr) == notSupported); Require(sceShareCaptureScreenshotExtended(nullptr, nullptr) == notSupported); + + using Capture = int (APS5_VABI*)(const void*, std::int32_t*); + for (Capture capture : {sceShareCaptureScreenshot, sceShareCaptureVideoClip}) { + reqId = 7; + Require(capture(param, &reqId) == notSupported); + Require(reqId == -1); + reqId = 7; + Require(capture(nullptr, &reqId) == notSupported); + Require(reqId == -1); + Require(capture(param, nullptr) == notSupported); + } + + Require(sceShareOpenMenuForContent(param) == notSupported); + Require(sceShareOpenMenuForContent(nullptr) == notSupported); + + constexpr std::int32_t invalidParam = static_cast(0x81960002); + std::uint8_t status[18]; + std::memset(status, 0x5a, sizeof(status)); + Require(sceShareGetCurrentStatus(1, status) == 0); + for (std::size_t index = 0; index < sizeof(status); ++index) Require(status[index] == (index < 16 ? 0 : 0x5a)); + std::memset(status, 0x5a, sizeof(status)); + Require(sceShareGetCurrentStatus(0xffffffffu, status) == 0); + Require(status[0] == 0 && status[15] == 0 && status[16] == 0x5a); + std::memset(status, 0x5a, sizeof(status)); + Require(sceShareGetCurrentStatus(0, status) == invalidParam); + Require(status[0] == 0x5a); + Require(sceShareGetCurrentStatus(1, nullptr) == invalidParam); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..c0cdd61f5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -94,6 +94,7 @@ Throughout the project, every function at every stage either **does exactly what - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 +- [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown ### Functional From aed4f3a25cc7340188b668e81d4bc204ea943838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:47:16 +0200 Subject: [PATCH 36/43] feat(systemservice): implement PowerTick and ReportAbnormalTermination Co-Authored-By: Claude --- core/libs/prx/libSceSystemService/Export.cpp | 6 ++---- core/libs/tests/GuestSystemService.cpp | 8 ++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 205897fe2..577d3fb0a 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -96,8 +96,7 @@ int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, size_t bufS } int APS5_VABI sceSystemServicePowerTick(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceReceiveEvent(SystemServiceEvent* event) { @@ -109,8 +108,7 @@ int APS5_VABI sceSystemServiceReceiveEvent(SystemServiceEvent* event) { int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info) { (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void) { diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index fe937fc57..cd875318c 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -23,7 +23,15 @@ bool ParamGetStringThrows(int paramId, char* buf, std::size_t bufSize) { } +extern "C" int APS5_VABI sceSystemServicePowerTick(void); +extern "C" int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info); + int main() { + Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); + Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReportAbnormalTermination(nullptr) == SYSTEM_SERVICE_OK); + int info = 0; + Require(sceSystemServiceReportAbnormalTermination(&info) == SYSTEM_SERVICE_OK); Require(sceSystemServiceGetHdrToneMapLuminance(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); SystemServiceHdrToneMapLuminance luminance{-1.0f, -1.0f, -1.0f}; Require(sceSystemServiceGetHdrToneMapLuminance(&luminance) == SYSTEM_SERVICE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..bfc5b1e49 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -18,6 +18,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing +- [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick` and `sceSystemServiceReportAbnormalTermination` return success and do nothing: the host has no auto power-off timer to reset and no system crash reporter to notify - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. From 23c8558c70e0b38d68727624255a1d76545491ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 19:44:32 +0200 Subject: [PATCH 37/43] fix(contentexport): name the AOWqIYsgVHs stub sceContentExportFromData Co-Authored-By: Claude --- core/libs/prx/libSceContentExport/Export.cpp | 11 +++++------ docs/dev/TechnicalDebt.md | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index 3887de0bc..1a7fde306 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -26,6 +26,11 @@ int APS5_VABI sceContentExportFinish(void) { return 0; } +int APS5_VABI sceContentExportFromData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceContentExportFromFile(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -47,10 +52,4 @@ int APS5_VABI sceContentExportTerm(void) { return 0; } -APS5_EXPORT("AOWqIYsgVHs", sceContentExportUnknown_AOWqIYsgVHs); -int32_t APS5_VABI sceContentExportUnknown_AOWqIYsgVHs(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..61233cfd0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -65,7 +65,6 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device -- [AOWqIYsgVHs](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - unknown name, signature - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask From 58263b50758d2b1f851091f96405807527575581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Mon, 5 Oct 2026 20:44:13 +0200 Subject: [PATCH 38/43] feat(libSceIme): implement keyboard info and mode queries Co-Authored-By: Claude --- core/libs/prx/libSceIme/CMakeLists.txt | 10 ++++ core/libs/prx/libSceIme/Export.cpp | 20 +++++--- core/libs/tests/GuestImeKeyboard.cpp | 67 ++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 core/libs/tests/GuestImeKeyboard.cpp diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 77f03e7ea..a34676059 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -18,4 +18,14 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_parameters PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_ime_keyboard_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeKeyboard.cpp) + target_include_directories(guest_ime_keyboard_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_keyboard_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_keyboard_tests) + add_test(NAME guest_ime_keyboard COMMAND guest_ime_keyboard_tests) + if(WIN32) + set_tests_properties(guest_ime_keyboard PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 150180112..db1dd7a85 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -10,12 +10,18 @@ namespace { +constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorNoResourceId = static_cast(0x80bc0023u); +constexpr int ErrorInvalidMode = static_cast(0x80bc0024u); +constexpr uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; constexpr uint32_t OptionUseOver2K = 0x00004000; +constexpr int32_t UserIdInvalid = -1; std::mutex g_keyboardMutex; std::set g_openKeyboards; @@ -47,9 +53,9 @@ int APS5_VABI sceImeKeyboardClose(int32_t user_id) { int APS5_VABI sceImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info) { (void)resource_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!info) return ErrorInvalidAddress; + std::lock_guard lock(g_keyboardMutex); + return g_openKeyboards.empty() ? ErrorNotOpened : ErrorNoResourceId; } int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids) { @@ -67,10 +73,10 @@ int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { } int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { - (void)user_id; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; + if (user_id == UserIdInvalid) return ErrorInvalidUserId; + std::lock_guard lock(g_keyboardMutex); + if (!g_openKeyboards.contains(user_id)) return ErrorNotOpened; + return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { diff --git a/core/libs/tests/GuestImeKeyboard.cpp b/core/libs/tests/GuestImeKeyboard.cpp new file mode 100644 index 000000000..cd3dbaf24 --- /dev/null +++ b/core/libs/tests/GuestImeKeyboard.cpp @@ -0,0 +1,67 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param); +int APS5_VABI sceImeKeyboardClose(int32_t user_id); +int APS5_VABI sceImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info); +int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode); +} + +static_assert(sizeof(KeyboardInfo) == 36); + +constexpr int NotOpened = static_cast(0x80bc0002u); +constexpr int InvalidUserId = static_cast(0x80bc0010u); +constexpr int NoResourceId = static_cast(0x80bc0023u); +constexpr int InvalidMode = static_cast(0x80bc0024u); +constexpr int InvalidAddress = static_cast(0x80bc0031u); + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static void RequireUntouched(const KeyboardInfo& info) { + const auto* bytes = reinterpret_cast(&info); + for (size_t i = 0; i < sizeof(info); ++i) Require(bytes[i] == 0xa5, "keyboard info written"); +} + +static void CheckClosed() { + KeyboardInfo info; + std::memset(&info, 0xa5, sizeof(info)); + Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info"); + Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info without an open keyboard"); + RequireUntouched(info); + Require(sceImeKeyboardSetMode(-1, 0) == InvalidUserId, "set mode invalid user"); + Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode without an open keyboard"); + Require(sceImeKeyboardSetMode(1, 0x80) == NotOpened, "set mode checks the mode before the keyboard"); +} + +static void CheckOpened() { + KeyboardParam param{}; + Require(sceImeKeyboardOpen(1, ¶m) == 0, "keyboard open failed"); + KeyboardInfo info; + std::memset(&info, 0xa5, sizeof(info)); + Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info with an open keyboard"); + Require(sceImeKeyboardGetInfo(0, &info) == NoResourceId, "resource id 0 reported as a keyboard"); + Require(sceImeKeyboardGetInfo(0x12345678, &info) == NoResourceId, "unknown resource id reported as a keyboard"); + RequireUntouched(info); + Require(sceImeKeyboardSetMode(-1, 0) == InvalidUserId, "set mode invalid user with an open keyboard"); + Require(sceImeKeyboardSetMode(2, 0) == NotOpened, "set mode for a user without an open keyboard"); + Require(sceImeKeyboardSetMode(1, 0) == 0, "mode 0 rejected"); + Require(sceImeKeyboardSetMode(1, 0x7f) == 0, "all mode bits rejected"); + Require(sceImeKeyboardSetMode(1, 0x41) == 0, "manual mode without format characters rejected"); + Require(sceImeKeyboardSetMode(1, 0x80) == InvalidMode, "mode bit 7 accepted"); + Require(sceImeKeyboardSetMode(1, 0x80000001u) == InvalidMode, "mode bit 31 accepted"); + Require(sceImeKeyboardClose(1) == 0, "keyboard close failed"); + Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info after close"); + Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode after close"); +} + +int main() { + CheckClosed(); + CheckOpened(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..c6a99d792 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -120,6 +120,7 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. +- [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero): reading it from the frame's Xing/Info, VBRI or Fraunhofer data is not implemented. - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. From 23c89852731d350b6b2cbdde4c45ee1e4aa3c27e Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:35:37 +0200 Subject: [PATCH 39/43] feat(shader): accept vcc as the scalar base address of global instructions The FLAT decoder took a scalar base address (SADDR) only from a numbered SGPR pair, so global loads, stores and atomics and global_load/store_dword_addtid with SADDR = vcc (106) threw "FLAT scalar address register range overflow". LLVM's GFX10 global saddr operand accepts s, vcc, ttmp, null and off. A two-register base may now be VCC. The address translation already reads it as VCC_LO:VCC_HI through offsetOperand. The same helper serves the scratch SADDR, where vcc_lo becomes the 32-bit offset. VCC_HI, M0, EXEC and the trap temporaries still throw. --- .../tests/execution/GlobalVccBase.cpp | 192 ++++++++++++++++++ .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 3 + 2 files changed, 195 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp new file mode 100644 index 000000000..e0e441989 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp @@ -0,0 +1,192 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t InputDwords = 768; +constexpr std::uint32_t Fill = 0xcdcdcdcdu; +constexpr std::uint32_t VectorBase = 512; +constexpr std::uint32_t DwordBase = 4; +constexpr std::uint32_t AddtidStoreBase = 256; +constexpr std::uint32_t AddtidBase = 384; +constexpr std::uint32_t AtomicBase = 448; +constexpr std::uint32_t OutputBase = 1024; +constexpr std::uint32_t OutputDwords = 8; + +alignas(256) constexpr std::array VccBaseCode{ + 0xbeea0400, 0x34020082, 0x34040084, 0x340c0085, 0x4a0c0cff, 0x00001000, 0x4ad404ff, 0x00000048, + 0xdc308010, 0x046a0001, 0xdcc98700, 0x0c6a0201, 0xdc3887b8, 0x006a006a, 0xbf8c3f70, 0xdc788000, + 0x006a0006, 0xdc708010, 0x006a0406, 0xdc708014, 0x006a0c06, 0xbf810000, +}; + +alignas(256) constexpr std::array VccAddtidCode{ + 0xbeea0400, 0x340c0085, 0x4a0c0cff, 0x00001000, 0xdc588600, 0x056a0000, 0xbf8c3f70, 0xdc708018, + 0x006a0506, 0xdc5c8400, 0x006a0500, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "global vcc base: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::vector Initial() { + std::vector memory(BlockBytes / 4u, Fill); + for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x51000000u + dword * 0x00010203u; + return memory; +} + +std::vector ExpectedBase() { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* out = &memory[OutputBase + tid * OutputDwords]; + for (std::uint32_t component = 0; component < 4u; ++component) out[component] = initial[VectorBase + tid * 4u + component]; + out[4] = initial[DwordBase + tid]; + out[5] = initial[AtomicBase + tid]; + memory[AtomicBase + tid] = initial[AtomicBase + tid] + tid * 16u; + } + return memory; +} + +std::vector ExpectedAddtid(std::uint32_t waveSize) { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t lane = tid % waveSize; + memory[OutputBase + tid * OutputDwords + 6u] = initial[AddtidBase + lane]; + memory[AddtidStoreBase + lane] = initial[AddtidBase + lane]; + } + return memory; +} + +ShaderRecompiler::RecompileResult Recompile(std::span code, std::uint32_t waveSize, std::span userData, const ShaderRecompiler::SpirvTarget& target) { + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::span code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::vector& expected, const std::string& name) { + auto memory = Initial(); + std::memcpy(guest.Data(), memory.data(), BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + std::vector userData(2, 0u); + userData[0] = static_cast(address); + userData[1] = static_cast(address >> 32u); + const auto result = Recompile(code, waveSize, userData, target); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::memcpy(memory.data(), guest.Data(), BlockBytes); + for (std::size_t dword = 0; dword < memory.size(); ++dword) { + Require(memory[dword] == expected[dword], "global vcc base: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword])); + } +} + +void CheckRejected(const AgcDriver::VulkanDevice& device, std::span code, const std::string& reason) { + const std::vector userData(2, 0u); + std::string failure; + try { + static_cast(Recompile(code, 32, userData, device.Target())); + } catch (const std::exception& error) { + failure = error.what(); + } + Require(failure.find(reason) != std::string::npos, "global vcc base: expected '" + reason + "', got '" + failure + "'"); +} + +void CheckRejections(const AgcDriver::VulkanDevice& device) { + alignas(256) static constexpr std::array vccHiLoad{0xdc308000, 0x046b0001, 0xbf810000}; + alignas(256) static constexpr std::array m0Atomic{0xdcc88000, 0x007c0201, 0xbf810000}; + alignas(256) static constexpr std::array execStore{0xdc708000, 0x007e0201, 0xbf810000}; + alignas(256) static constexpr std::array vccHiAddtid{0xdc588000, 0x056b0000, 0xbf810000}; + CheckRejected(device, vccHiLoad, "FLAT scalar address register range overflow"); + CheckRejected(device, m0Atomic, "FLAT scalar address register range overflow"); + CheckRejected(device, execStore, "FLAT scalar address register range overflow"); + CheckRejected(device, vccHiAddtid, "global_load_dword_addtid supports only an SGPR pair as base address"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckRejections(*device); + GuestBlock guest; + const auto base = ExpectedBase(); + Run(*device, guest, VccBaseCode, 32, device->Target(), base, "wave32"); + Run(*device, guest, VccBaseCode, 64, device->Target(), base, "wave64"); + Run(*device, guest, VccBaseCode, 64, device->ComputeTarget(32), base, "wave64 split"); + if (device->Target().subgroupSize < 32u) { + std::printf("addtid cases skipped, the device's subgroups are narrower than a wave (%u lanes)\n", device->Target().subgroupSize); + } else { + Run(*device, guest, VccAddtidCode, 32, device->Target(), ExpectedAddtid(32), "addtid wave32"); + Run(*device, guest, VccAddtidCode, 64, device->Target(), ExpectedAddtid(64), "addtid wave64"); + Run(*device, guest, VccAddtidCode, 64, device->ComputeTarget(32), ExpectedAddtid(64), "addtid wave64 split"); + } + std::puts("global vcc base tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 2544e10eb..a30ed3d3a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -434,6 +434,9 @@ RdnaOperand scalarDestination(std::uint32_t code) { RdnaOperand scalarDescriptorBase(std::uint32_t reg, std::uint32_t registerCount, const char* reason) { const auto operand = scalarSource(reg); + if (registerCount == 2u && operand.kind == RdnaOperandKind::VccLo) { + return operand; + } if (operand.kind != RdnaOperandKind::ScalarRegister || reg + (registerCount - 1u) > 105u) { throw std::runtime_error(reason); } From 4a67b140fc012e75118309f74154eedc3a13674f Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:35:43 +0200 Subject: [PATCH 40/43] feat(agc): accept conservative Z export in DB_SHADER_CONTROL DecodeState, the draw precheck and DecodePixelStageInfo rejected every nonzero CONSERVATIVE_Z_EXPORT (DB_SHADER_CONTROL bits 13-14). EXPORT_LESS_THAN_Z (1) and EXPORT_GREATER_THAN_Z (2) promise that the exported Z stays on one side of the interpolated Z. The value now reaches the recompiler as a pixel stage field, and a pixel shader that writes FragDepth declares DepthLess or DepthGreater next to DepthReplacing, the SPIR-V modes RADV maps to these values. Vulkan lets those modes only add a depth test of the interpolated Z before shading, so a shader that keeps the promise renders the same. The reserved value 3 still throws. The field is part of the recompile cache key, the draw key and the serialized request (version 7), and ValidateShaders accepts both modes. --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/src/ShaderInputState.cpp | 6 +- .../Graphics/src/ShaderValidation.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 79 +++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 14 +++- .../Optimization/ShaderStageInputInfo.hpp | 2 + core/shader/recompiler/Recompiler.hpp | 7 ++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 6 ++ .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/TechnicalDebt.md | 1 + 13 files changed, 117 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 64ec8646f..cdc931174 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -62,6 +62,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); std::lock_guard cacheLock(drawCacheMutex); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 27d567cd6..5181d61f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -81,7 +81,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 44ee5932f..9b814bf83 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -118,8 +118,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const auto shaderControl = read(context, dbShaderControl, RegisterBank::Context); // Bits 9 and 11 are EXEC_ON_HIER_FAIL and ALPHA_TO_MASK_DISABLE on GFX10; neither changes what the // recompiled pixel shader computes. - if (((shaderControl >> 13u) & 0x3u) != 0) { - throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT is unsupported"); + const auto conservativeZExport = (shaderControl >> 13u) & 0x3u; + if (conservativeZExport == 3u) { + throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT uses the reserved value 3"); } const auto colFormat = nullProgram ? readOr(context, spiShaderColFormat, RegisterBank::Context, 0u) : read(context, spiShaderColFormat, RegisterBank::Context); std::array targetOutputMode{}; @@ -152,6 +153,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .sampleMaskExportEnable = sampleMaskExportEnable, .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, + .conservativeZExport = static_cast(conservativeZExport), .targetOutputMode = targetOutputMode, .targetExportMapping = exportMappings }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index b489fce1b..67a61cc4e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -368,7 +368,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (fragment) { - for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests), "unsupported fragment execution mode"); + for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; bool push = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c8d156bf5..02a893d26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -64,7 +64,7 @@ constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. -constexpr std::uint32_t ShaderControlMask = ~(0x00009870u | 0x00020600u); +constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u); constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t ScanModeMask = ~2u; constexpr std::uint32_t ScanControlMask = ~0x06003fffu; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 75b240db0..f151c35e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "CacheKey.hpp" #include #include #include @@ -1283,6 +1284,7 @@ void misalignedShaderDataTests() { struct ModuleShape { bool fragment = false; + std::optional fragmentMode; bool push = false; std::uint32_t pushLength = 32; std::uint32_t pushStride = 4; @@ -1446,6 +1448,7 @@ std::vector makeModule(const ModuleShape& shape) { else emit(words, spv::OpEntryPoint, {shape.fragment ? spv::ExecutionModelFragment : spv::ExecutionModelVertex, main, 0x6e69616du, 0, output}); words[entryPointOffset] += static_cast(extraInterface.size()) << 16u; words.insert(words.end(), extraInterface.begin(), extraInterface.end()); + if (shape.fragmentMode) emit(words, spv::OpExecutionMode, {main, *shape.fragmentMode}); if (shape.fragment) emit(words, spv::OpExecutionMode, {main, spv::ExecutionModeOriginUpperLeft}); words.insert(words.end(), annotations.begin(), annotations.end()); words.insert(words.end(), declarations.begin(), declarations.end()); @@ -1563,6 +1566,81 @@ std::vector locatedInputs(std::span words) { return result; } +constexpr std::array DepthExportCode{0xf8001881u, 2u, 0xbf810000u}; + +AgcDriver::QueueState depthExportQueue(std::uint32_t shaderControl) { + auto queue = makeState(); + queue.context[0x1b3] = 0x402u; + queue.context[0x1b4] = 0x402u; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest depthExportRequest(std::uint32_t shaderControl) { + const auto queue = depthExportQueue(shaderControl); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, DepthExportCode, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +std::set depthModes(const ShaderRecompiler::RecompileRequest& request) { + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + std::set modes; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) != spv::OpExecutionMode) continue; + const auto mode = words[at + 2]; + if (mode == spv::ExecutionModeDepthReplacing || mode == spv::ExecutionModeDepthGreater || mode == spv::ExecutionModeDepthLess || mode == spv::ExecutionModeDepthUnchanged) modes.insert(mode); + } + return modes; +} + +void conservativeZExportTests() { + const std::array>, 4> cases{{ + {0x0021u, {spv::ExecutionModeDepthReplacing}}, + {0x2021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthLess}}, + {0x4021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthGreater}}, + {0x2020u, {}} + }}; + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + for (const auto& [shaderControl, expected] : cases) { + const auto request = depthExportRequest(shaderControl); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(depthModes(request) == expected && depthModes(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong depth execution modes"); + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + keys.insert(key); + } + Require(keys.size() == cases.size(), "the recompile cache key ignores CONSERVATIVE_Z_EXPORT"); + for (const auto shaderControl : {0x2020u, 0x4020u}) { + const auto queue = depthExportQueue(shaderControl); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl)); + } + expectFailure([] { static_cast(depthExportRequest(0x6021u)); }, "CONSERVATIVE_Z_EXPORT"); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + ShaderRecompiler::RecompileResult pixel; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + for (const auto mode : {spv::ExecutionModeDepthLess, spv::ExecutionModeDepthGreater}) { + pixel.spirv = makeModule({.fragment = true, .fragmentMode = mode}); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); + } + pixel.spirv = makeModule({.fragment = true, .fragmentMode = spv::ExecutionModeDepthUnchanged}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode"); +} + void pixelParameterSlotTests() { using AgcDriver::Graphics::CompiledShader; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; @@ -1922,6 +2000,7 @@ int main() { DepthClipTests(); DepthStencilTests(); DepthBoundsBiasTests(); + conservativeZExportTests(); DisabledColorTests(); CompactedExportTests(); metadataPassTests(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 79fee172b..800fdbbbd 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -140,6 +140,7 @@ private: append(key, value.sampleMaskExportEnable); append(key, value.earlyZ); append(key, value.executeOnNoop); + append(key, value.conservativeZExport); append(key, value.targetOutputMode); append(key, value.targetExportMapping); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 21ac3eddf..0e22804d0 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -227,6 +227,14 @@ ProgramRole readProgramRole(Reader& reader) { return static_cast(value); } +ConservativeZExport readConservativeZExport(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(ConservativeZExport::GreaterThanZ)) { + throw std::runtime_error("RequestSerializer: invalid ConservativeZExport value"); + } + return static_cast(value); +} + void writeShaderBinary(Writer& writer, const ShaderBinary& binary) { writer.WriteU8(static_cast(binary.stage)); writer.WriteU64(binary.codeAddress); @@ -296,6 +304,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.sampleMaskExportEnable); writer.WriteBool(info.earlyZ); writer.WriteBool(info.executeOnNoop); + writer.WriteU8(static_cast(info.conservativeZExport)); for (const std::uint8_t value : info.targetOutputMode) { writer.WriteU8(value); } @@ -328,6 +337,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.sampleMaskExportEnable = reader.ReadBool(); info.earlyZ = reader.ReadBool(); info.executeOnNoop = reader.ReadBool(); + if (version >= 7u) info.conservativeZExport = readConservativeZExport(reader); for (std::uint8_t& value : info.targetOutputMode) { value = reader.ReadU8(); } @@ -677,7 +687,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(6u); + writer.WriteU32(7u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -701,7 +711,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 6u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 7u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 3d9c39d7f..befe1cce7 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_SHADERSTAGEINPUTINFO_HPP #include "IntermediateRepresentation/IrMetadata.hpp" +#include "Recompiler.hpp" #include #include #include @@ -183,6 +184,7 @@ struct ShaderPixelInputInfo { bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; + ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 054374fb5..1b3aff3a7 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -98,6 +98,12 @@ constexpr std::uint32_t PixelInputVgpr(std::uint32_t inputAddr, PixelInput input return vgpr; } +enum class ConservativeZExport : std::uint8_t { + AnyZ, + LessThanZ, + GreaterThanZ +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -119,6 +125,7 @@ struct ShaderPixelStageInfo { bool sampleMaskExportEnable; bool earlyZ; bool executeOnNoop; + ConservativeZExport conservativeZExport; std::array targetOutputMode; std::array targetExportMapping; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9b56e98ae..9100ce94f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -197,6 +197,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOriginUpperLeft); if (state.depthVariable != 0) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthReplacing); + if (pixel.psConservativeZExport == ConservativeZExport::LessThanZ) { + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthLess); + } + if (pixel.psConservativeZExport == ConservativeZExport::GreaterThanZ) { + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); + } } if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 32887b3bc..81327c49f 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -137,6 +137,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; + pixelStorage.psConservativeZExport = pixel.conservativeZExport; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2ddee4350..9c4cc6a26 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -114,6 +114,7 @@ Throughout the project, every function at every stage either **does exactly what - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink. - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. +- [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. From 5f689998365d2936d850e148225968929c2312f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 00:44:37 +0200 Subject: [PATCH 41/43] feat(agc): implement sceAgcCondExecPatchSetCommandAddress and sceAgcCondExecPatchSetEnd Co-Authored-By: Claude --- core/libs/prx/libSceAgc/CMakeLists.txt | 20 ++++ .../libs/prx/libSceAgc/Patch/src/CondExec.cpp | 22 ++-- .../prx/libSceAgc/tests/CondExecPatch.cpp | 107 ++++++++++++++++++ 3 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/CondExecPatch.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index f1822c8ce..aadb3fdb0 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -200,3 +200,23 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_cond_exec_patch_tests EXCLUDE_FROM_ALL + tests/CondExecPatch.cpp + Command/src/Packet.cpp + Command/src/Memory.cpp + Command/src/Control.cpp + DcbFlow/src/Sync.cpp + Patch/src/CondExec.cpp +) +target_include_directories(agc_cond_exec_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_cond_exec_patch_tests PRIVATE libc) +configure_windows_unwind(agc_cond_exec_patch_tests) +if(BUILD_TESTING) + set_target_properties(agc_cond_exec_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_cond_exec_patch_tests COMMAND agc_cond_exec_patch_tests) + if(WIN32) + set_tests_properties(agc_cond_exec_patch_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp index 42b45d366..bae2206ad 100644 --- a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp @@ -10,17 +10,23 @@ extern "C" { int APS5_VABI sceAgcCondExecPatchSetCommandAddress(uint32_t* cmd, const volatile uint32_t* command) { - (void)cmd; - (void)command; - NotImplemented_nid_no_patch(__func__); - return 0; + Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__); + const auto address = reinterpret_cast(command); + Agc::Command::CheckAddress(address, 4, __func__); + cmd[1] = (cmd[1] & 3u) | (static_cast(address) & ~3u); + cmd[2] = static_cast(address >> 32u); + return 0; } int APS5_VABI sceAgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer) { - (void)cmd; - (void)buffer; - NotImplemented_nid_no_patch(__func__); - return 0; + Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__); + const auto end = reinterpret_cast(buffer); + const auto packetEnd = reinterpret_cast(cmd + 5); + Agc::Command::CheckAddress(end, 4, __func__); + const auto numDwords = (end - packetEnd) / sizeof(std::uint32_t); + Agc::Command::Require(numDwords <= 0x3fffu, __func__, "conditional execution range is too large"); + cmd[4] = (cmd[4] & ~0x3fffu) | static_cast(numDwords); + return 0; } int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress(std::uint32_t* cmd, const volatile std::uint32_t* command) { diff --git a/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp new file mode 100644 index 000000000..f338beea7 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp @@ -0,0 +1,107 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbCondExec(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t); +int APS5_VABI sceAgcCondExecPatchSetCommandAddress(std::uint32_t*, const volatile std::uint32_t*); +int APS5_VABI sceAgcCondExecPatchSetEnd(std::uint32_t*, const volatile std::uint32_t*); +} + +namespace { + +constexpr std::uint32_t Sentinel = 0xabcdef01u; + +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"); +} + +const volatile std::uint32_t* at(std::uintptr_t address) { + return reinterpret_cast(address); +} + +struct Storage { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + std::uint32_t condition = 1; + std::uint32_t* packet = nullptr; + + Storage() { + words.fill(Sentinel); + packet = sceAgcDcbCondExec(&buffer, &condition, 3); + check(packet == words.data() && packet[0] == 0xc0032200u && packet[4] == 3, "unexpected COND_EXEC packet"); + } +}; + +std::uintptr_t endAfter(const std::uint32_t* packet, std::uintptr_t numDwords) { + return reinterpret_cast(packet + 5) + numDwords * sizeof(std::uint32_t); +} + +template +void expectUnchanged(TAction action) { + Storage storage; + const auto before = storage.words; + expectFailure([&] { action(storage.packet); }); + check(storage.words == before, "failed patch modified the packet"); +} + +void testSetEnd() { + for (const std::uintptr_t numDwords : {0u, 1u, 7u, 0x3fffu}) { + Storage storage; + const auto before = storage.words; + check(sceAgcCondExecPatchSetEnd(storage.packet, at(endAfter(storage.packet, numDwords))) == 0, "SetEnd failed"); + check(storage.packet[4] == numDwords, "SetEnd wrote the wrong dword count"); + check(std::equal(before.begin(), before.begin() + 4, storage.words.begin()), "SetEnd modified other packet words"); + check(std::equal(before.begin() + 5, before.end(), storage.words.begin() + 5), "SetEnd modified following words"); + } + + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, at(endAfter(packet, 0x4000u))); }); + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, packet + 4); }); + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, nullptr); }); + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, at(endAfter(packet, 2) + 2)); }); + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet + 1, at(endAfter(packet, 2))); }); +} + +void testSetCommandAddress() { + for (const std::uintptr_t address : {std::uintptr_t{0x0000123456789abcu}, std::uintptr_t{4u}, std::uintptr_t{0x0000fffffffffffcu}}) { + Storage storage; + const auto before = storage.words; + check(sceAgcCondExecPatchSetCommandAddress(storage.packet, at(address)) == 0, "SetCommandAddress failed"); + check(storage.packet[1] == static_cast(address) && storage.packet[2] == static_cast(address >> 32u), "SetCommandAddress wrote the wrong address"); + check(storage.packet[0] == before[0] && storage.packet[3] == before[3] && storage.packet[4] == before[4], "SetCommandAddress modified other packet words"); + check(std::equal(before.begin() + 5, before.end(), storage.words.begin() + 5), "SetCommandAddress modified following words"); + } + + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet, nullptr); }); + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet, at(0x1002u)); }); + expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet + 1, at(0x1000u)); }); +} + +} + +int main() { + try { + testSetEnd(); + testSetCommandAddress(); + std::puts("AGC COND_EXEC patch tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From 40c191029d0880e3fc582cfa69ccc2fbbc4fbeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 00:50:27 +0200 Subject: [PATCH 42/43] feat(agc): implement sceAgcGetPacketSize Co-Authored-By: Claude --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 ++++ .../prx/libSceAgc/Misc/src/PacketInfo.cpp | 9 ++- core/libs/prx/libSceAgc/tests/PacketInfo.cpp | 66 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/PacketInfo.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index f1822c8ce..8553cfd66 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -83,6 +83,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp) +target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_packet_info_tests) +if(BUILD_TESTING) + set_target_properties(agc_packet_info_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_packet_info_tests COMMAND agc_packet_info_tests) + if(WIN32) + set_tests_properties(agc_packet_info_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_register_command_tests EXCLUDE_FROM_ALL tests/RegisterCommands.cpp Command/src/Packet.cpp diff --git a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp index 12f74258a..a0d16bb57 100644 --- a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp @@ -9,9 +9,12 @@ extern "C" { uint32_t APS5_VABI sceAgcGetPacketSize(uint32_t* packet) { - (void)packet; - NotImplemented_nid_no_patch(__func__); - return 0; + Agc::Command::CheckAddress(reinterpret_cast(packet), alignof(uint32_t), __func__); + Agc::Command::Require((packet[0] & 0xc0000000u) == 0xc0000000u, __func__, "source pointer is not a PM4 packet"); + if ((packet[0] & 0x3fffff00u) == 0x3fff1000u) { + return 1; + } + return ((packet[0] >> 16u) & 0x3fffu) + 2u; } APS5_EXPORT("V++UgBtQhn0", sceAgcGetDataPacketPayloadAddressUnk); diff --git a/core/libs/prx/libSceAgc/tests/PacketInfo.cpp b/core/libs/prx/libSceAgc/tests/PacketInfo.cpp new file mode 100644 index 000000000..5e95906b5 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/PacketInfo.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include + +extern "C" std::uint32_t APS5_VABI sceAgcGetPacketSize(std::uint32_t* packet); + +namespace { + +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& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error("expected an exception"); +} + +void testPacketSize() { + const std::array, 7> sizes{{ + {0xc0047600u, 6}, {0xc0001000u, 2}, {0xfffe1000u, 0x4000}, {0xffff7600u, 0x4001}, + {0xffff1000u, 1}, {0xffff1001u, 1}, {0xffff10fcu, 1}}}; + for (const auto& [header, size] : sizes) { + std::array words{header, 0xffff1000u}; + check(sceAgcGetPacketSize(words.data()) == size, "packet size mismatch"); + check(words[0] == header && words[1] == 0xffff1000u, "packet size query modified the packet"); + } +} + +void testRejections() { + for (const auto header : {0x80000000u, 0x3fff1000u, 0x40001000u, 0x00047600u}) { + std::uint32_t word = header; + expectFailure([&] { sceAgcGetPacketSize(&word); }); + } + expectFailure([] { sceAgcGetPacketSize(nullptr); }); + std::array words{0x00100000u, 0x000000c0u}; + auto* misaligned = reinterpret_cast(reinterpret_cast(words.data()) + 1); + expectFailure([&] { sceAgcGetPacketSize(misaligned); }); +} + +} + +int main() { + try { + testPacketSize(); + testRejections(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC packet info tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} From 9322634817dd3ba74ff8b664574a0a736dfa8932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 00:54:31 +0200 Subject: [PATCH 43/43] feat(agc): implement sceAgcGetDataPacketPayloadRange Co-Authored-By: Claude --- core/libs/prx/libSceAgc/CMakeLists.txt | 13 +++ .../prx/libSceAgc/Misc/include/PacketInfo.hpp | 7 ++ .../prx/libSceAgc/Misc/src/PacketInfo.cpp | 17 ++++ core/libs/prx/libSceAgc/Unimplemented.cpp | 5 -- .../libs/prx/libSceAgc/tests/PayloadRange.cpp | 80 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgc/tests/PayloadRange.cpp diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index f1822c8ce..c5261ce3a 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -102,6 +102,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_payload_range_tests EXCLUDE_FROM_ALL tests/PayloadRange.cpp) +target_include_directories(agc_payload_range_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_payload_range_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_payload_range_tests) +if(BUILD_TESTING) + set_target_properties(agc_payload_range_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_payload_range_tests COMMAND agc_payload_range_tests) + if(WIN32) + set_tests_properties(agc_payload_range_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL tests/BranchPatch.cpp Command/src/Packet.cpp diff --git a/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp b/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp index 535ebefb4..85a76dced 100644 --- a/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp +++ b/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp @@ -1,4 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP #define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP +#include + +struct SceAgcMemoryRange { + void* base; + uint64_t size; +}; + #endif diff --git a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp index 12f74258a..6c398bc17 100644 --- a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp @@ -26,6 +26,23 @@ int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(uint32_t** addr, uint32_t* cm return 0; } +int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, uint32_t* cmd, int type) { + Agc::Command::CheckAddress(reinterpret_cast(range), alignof(SceAgcMemoryRange), __func__); + Agc::Command::CheckAddress(reinterpret_cast(cmd), alignof(uint32_t), __func__); + const uint64_t count_bytes = (cmd[0] >> 14u) & 0xfffcu; + if (type != 0) { + range->base = cmd + 2; + range->size = count_bytes; + } else if ((~cmd[0] & 0x3fff0000u) == 0) { + range->base = nullptr; + range->size = 0; + } else { + range->base = cmd + 1; + range->size = count_bytes + sizeof(uint32_t); + } + return 0; +} + int APS5_VABI sceAgcDcbGetSystemSoftwareVersion(uint32_t* destination, const uint32_t* packet) { Agc::Command::CheckAddress(reinterpret_cast(destination), alignof(uint32_t), __func__); Agc::Command::CheckAddress(reinterpret_cast(packet), alignof(uint32_t), __func__); diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index dea38a6a4..52413cc77 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -55,11 +55,6 @@ int APS5_VABI sceAgcDebugRaiseException() { return 0; } -int APS5_VABI sceAgcGetDataPacketPayloadRange() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcGetDefaultCxStateFlat() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAgc/tests/PayloadRange.cpp b/core/libs/prx/libSceAgc/tests/PayloadRange.cpp new file mode 100644 index 000000000..97a386cc3 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/PayloadRange.cpp @@ -0,0 +1,80 @@ +#include "prx/libSceAgc/Misc/include/PacketInfo.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, std::uint32_t* cmd, int type); +extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type); + +namespace { + +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& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error("expected an exception"); +} + +void testRanges() { + const std::array, 10> rows{{ + {0xc0047600u, 1, 2, 16}, {0xc0001000u, 1, 2, 0}, {0xffff7600u, 1, 2, 0xfffc}, {0xc0047600u, -1, 2, 16}, + {0xc0047600u, 7, 2, 16}, {0xc0021000u, 0, 1, 12}, {0xc0001000u, 0, 1, 4}, {0xfffe1000u, 0, 1, 0xfffc}, + {0xffff1000u, 0, -1, 0}, {0xffff7600u, 0, -1, 0}}}; + for (const auto& [header, type, offset, size] : rows) { + std::array words{header, 0x12345678u}; + SceAgcMemoryRange range{reinterpret_cast(std::uintptr_t{0x1000}), 0xdeadu}; + check(sceAgcGetDataPacketPayloadRange(&range, words.data(), type) == 0, "payload range failed"); + void* expected = offset < 0 ? nullptr : static_cast(words.data() + offset); + check(range.base == expected, "payload range base mismatch"); + check(range.size == size, "payload range size mismatch"); + check(words[0] == header && words[1] == 0x12345678u, "payload range query modified the packet"); + std::uint32_t* address = nullptr; + check(sceAgcGetDataPacketPayloadAddressUnk(&address, words.data(), type) == 0 && address == range.base, + "payload range base differs from the payload address"); + } +} + +void testRejections() { + std::array words{0xc0047600u, 0u}; + SceAgcMemoryRange range{}; + expectFailure([&] { sceAgcGetDataPacketPayloadRange(nullptr, words.data(), 1); }); + expectFailure([&] { sceAgcGetDataPacketPayloadRange(&range, nullptr, 1); }); + auto* misaligned = reinterpret_cast(reinterpret_cast(words.data()) + 1); + expectFailure([&] { sceAgcGetDataPacketPayloadRange(&range, misaligned, 0); }); + std::array storage{}; + auto* misalignedRange = reinterpret_cast(storage.data() + 4); + if (reinterpret_cast(misalignedRange) % alignof(SceAgcMemoryRange) == 0) { + misalignedRange = reinterpret_cast(storage.data() + 2); + } + expectFailure([&] { sceAgcGetDataPacketPayloadRange(misalignedRange, words.data(), 1); }); +} + +} + +int main() { + try { + testRanges(); + testRejections(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC payload range tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5fd46685b..d42afe4cb 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -44,6 +44,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 - [dbOlWdppb4o](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [V++UgBtQhn0](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name +- [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `V++UgBtQhn0`; prosper returns header + 2 for both types from that address function - [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