From a9fa02d35db7605a7eb1396fc60f2a996c023a6f Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:02:58 +0000 Subject: [PATCH 01/22] fix(relinker): restore rbx in TLS coverage bodies run on the host The alu_execution and moved_branch fixtures write rbx and return to the host caller without restoring it. rbx is callee-saved in both the SysV and the Windows x64 ABI, so the ctypes caller in windows_image_runner.py may use the clobbered value after the call. With Python 3.9 on Linux x86-64 both groups crash with SIGSEGV; newer Python happens not to rely on rbx there. Save rbx on entry and restore it before every return, as the other executed fixtures already do for the callee-saved registers they use, and shift the patched TLS addresses and branch targets by the extra push. --- .../tests/test_tls_function_coverage.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 8061ee1638..f27ffb5dc2 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -58,6 +58,7 @@ def alu_check_body(opcode, register, base, value, result, flags_masked): emit(bytes.fromhex("0f 85 00 00 00 00")) jumps.append(len(code) - 4) + emit(b"\x53") emit(bytes.fromhex("64 c7 04 25 28 00 00 00") + struct.pack(" Date: Thu, 15 Oct 2026 16:15:05 +0100 Subject: [PATCH 02/22] perf(agc): clear single-key DCC surfaces instead of uploading them A fast clear whose value the code does not carry is not on the IsDccClear whitelist, so a ClearSingle surface fell through to a full host upload. For the 4K color targets a title clears through its DCC keys that is 63.8 MiB of guest read, staging fill and DMA per surface per frame -- about 101 times per 10 s, roughly a third of the frame budget in Chapter 1 of Bendy and The Ink Machine -- while the guest texel memory never changed. The value is already in the surface: a single-key surface carries its one color as the leading element of every 256-byte key block, which ReadTextureSurface expands over that block for a read. Read that one element instead and clear the image to it. FillClearColor validates the value and refuses rather than produce a wrong color, so anything it cannot express exactly keeps the old upload path. IsDccClear keeps its whitelist meaning for its other callers; only upload's fast-clear gate learns the additional code. Measured on an RX 7900 GRE, Release build, Chapter 1 (level1), no tuning switches: frame p50 94.2 -> 72.7 ms, p99 159.0 -> 90.4 ms, 309 -> 407 presents per 30 s (10.3 -> 13.5 fps), with gpu_busy unchanged at 38.6 ms per present. The [storage] uploads by path counters went from cpu 109 / 6948.8 MiB to cpu 0 / 0.0 per 10 s window, and the load-point timeline is unchanged. --- .../Graphics/include/DccMetadata.hpp | 2 ++ .../Graphics/src/DccMetadata.cpp | 17 +++++++++++++---- .../libSceAgcDriver/Graphics/src/Texture.cpp | 14 +++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index d2e9dff3a6..a055ac6657 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -79,6 +79,8 @@ struct DccKeyProof { // APS5_NO_KEY_FAST_PATH=1 scans on every call and stores nothing. DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, DccKeyProof& proof); bool KeyFastPath(); +bool SingleKeysReadable(const GuestTextureResource& resource); +bool ReadSingleElement(const GuestTextureResource& resource, std::span element); // Cumulative outcomes of ProvedClearKeys: calls answered by a proof, scans made, and scans not kept. struct DccKeyProofCounts { std::uint64_t proved; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index f8f4d371ec..ca28030b19 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -57,10 +57,6 @@ DccKeys ByteKeys(std::uint8_t key) { } } -bool SingleKeysReadable(const GuestTextureResource& resource) { - return KeyBytes % BytesPerElement(resource.format) == 0; -} - void ExpandSingleBlocks(std::span bytes, std::size_t elementBytes) { for (std::size_t block = 0; block < bytes.size(); block += KeyBytes) { const auto blockBytes = std::min(KeyBytes, bytes.size() - block); @@ -743,6 +739,19 @@ void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std: if (keys == DccKeys::ClearSingle) ExpandSingleBlocks(bytes, BytesPerElement(resource.format)); } +bool SingleKeysReadable(const GuestTextureResource& resource) { + return KeyBytes % BytesPerElement(resource.format) == 0; +} + +bool ReadSingleElement(const GuestTextureResource& resource, std::span element) { + if (!SingleKeysReadable(resource)) return false; + const auto bytes = BytesPerElement(resource.format); + if (element.size() < bytes) return false; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); + GuestMemory::ReadCommitted(resource.baseAddress, element.subspan(0, bytes)); + return true; +} + namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ba62bdcde6..7b6877d6ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2161,6 +2161,17 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; +bool SingleKeyColorFor(const GuestTextureResource& resource, VkFormat format, DccKeys keys, VkClearColorValue& clear) { + if (keys != DccKeys::ClearSingle) return false; + const auto elementBytes = BytesPerElement(resource.format); + if (elementBytes == 0 || elementBytes > sizeof(std::uint32_t) * 4) return false; + std::uint32_t pattern[4]{}; + auto* bytes = reinterpret_cast(pattern); + if (!ReadSingleElement(resource, std::span(bytes, elementBytes))) return false; + for (std::size_t at = elementBytes; at < sizeof(pattern); at += elementBytes) std::memcpy(bytes + at, bytes, elementBytes); + return FillClearColor(format, elementBytes, pattern, clear); +} + void StorageTexture::upload(const std::vector* layers) { const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -2192,7 +2203,8 @@ void StorageTexture::upload(const std::vector* layers) { uploadedKeys = ProvedClearKeys(descriptor, guestBytes, keyProof); filledKeys = DccKeys::Uncompressed; VkClearColorValue clearValue{}; - if (IsDccClear(uploadedKeys) && ClearColorFor(storageFormat, uploadedKeys, clearValue)) { + const bool fastClear = IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle; + if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, storageFormat, uploadedKeys, clearValue))) { // A fast-cleared surface is cleared on the GPU; its texel memory is neither read nor filled. // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms From 756f3aff3ba4904cd1f531a9ca3721f725ed62cd Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:54:01 +0100 Subject: [PATCH 03/22] fix(agc): require a uniformity proof for single-key DCC clears A ClearSingle surface carries its clear color as the leading element of every 256-byte DCC key block, but the keys do not guarantee that every block clears to that one color: agc_driver_dcc_keys_first_write fails on 9900a7ed7 with 16296/16384 texels wrong, exactly the stored block-0 color plus the 24 kernel texels that a key of all zeros leaves behind. Reading one element and latching it was unsound. ProvedSingleUniform in DccMetadata reads the first element of every 256-byte key block once per upload epoch, gathered in 1 MiB chunks (about 64 ReadCommitted calls instead of 261k one-element reads), bounded to the surface so the gather never reads past the keys it proves, requires every block-leading element to match the first, and memoizes the proof until the next key upload. StorageTexture::upload keeps the fast clear only on a proven single uniform color and falls back to the original CPU upload path otherwise. Any doubt is a fallback, never a wrong clear. The proof passes for the 4K color targets, so the fast clear stays live: the failing test now passes, full ctest is green, and a gated A/B in Bendy and The Ink Machine still shows the win, p50 116.4 -> 112.3 ms and p99 146.2 -> 117.8 ms per present (256 -> 267 presents per 30 s), because ClearSingle genuinely stamps every block each frame there and the sound gather costs about as much as the guest read it skips. --- .../Graphics/include/DccMetadata.hpp | 8 ++++++ .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/DccMetadata.cpp | 25 +++++++++++++++++++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 ++--- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index a055ac6657..b8441174ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -81,6 +81,14 @@ DccKeys ProvedClearKeys(const GuestTextureResource& resource, std::uint64_t gues bool KeyFastPath(); bool SingleKeysReadable(const GuestTextureResource& resource); bool ReadSingleElement(const GuestTextureResource& resource, std::span element); +struct DccSingleProof { + std::uint64_t address = 0; + std::uint64_t bytes = 0; + std::uint64_t generation = 0; + std::uint32_t format = 0; + bool uniform = false; +}; +bool ProvedSingleUniform(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof); // Cumulative outcomes of ProvedClearKeys: calls answered by a proof, scans made, and scans not kept. struct DccKeyProofCounts { std::uint64_t proved; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 5a70e2f069..ee3a18379f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -538,6 +538,7 @@ private: mutable DccKeys filledKeys = DccKeys::Uncompressed; mutable DccKeyProof keyProof; mutable DccRangeProof targetKeyProof; + DccSingleProof singleProof; struct RefreshProof { std::uint64_t generation = 0; std::uint64_t pendingSerial = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index ca28030b19..52167fbd4d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -752,6 +752,31 @@ bool ReadSingleElement(const GuestTextureResource& resource, std::span 16) return false; + const auto blocks = static_cast(guestBytes / KeyBytes); + if (proof.generation != 0 && proof.address == resource.baseAddress && proof.bytes == guestBytes && proof.format == resource.format && collected != 0 && GuestMemory::UnchangedSince(resource.baseAddress, static_cast(guestBytes), proof.generation)) return proof.uniform; + std::byte first[16]{}; + bool uniform = true; + std::vector buffer(std::min(guestBytes, std::uint64_t(1) << 20)); + { + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); + GuestMemory::ReadCommitted(resource.baseAddress, std::span(first, bytes)); + for (std::size_t block = 1; block < blocks && uniform;) { + const auto offset = block * KeyBytes; + const auto size = static_cast(std::min(buffer.size(), guestBytes - offset)); + GuestMemory::ReadCommitted(resource.baseAddress + offset, std::span(buffer).first(size)); + const auto covered = size / KeyBytes; + for (std::size_t index = 0; index < covered && uniform; ++index) uniform = std::equal(first, first + bytes, buffer.begin() + index * KeyBytes); + block += covered; + } + } + proof = collected != 0 ? DccSingleProof{resource.baseAddress, guestBytes, collected, resource.format, uniform} : DccSingleProof{}; + return uniform; +} + namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7b6877d6ea..7c249e0643 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2161,8 +2161,8 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; -bool SingleKeyColorFor(const GuestTextureResource& resource, VkFormat format, DccKeys keys, VkClearColorValue& clear) { - if (keys != DccKeys::ClearSingle) return false; +bool SingleKeyColorFor(const GuestTextureResource& resource, std::uint64_t guestBytes, std::uint64_t collected, DccSingleProof& proof, VkFormat format, DccKeys keys, VkClearColorValue& clear) { + if (keys != DccKeys::ClearSingle || !ProvedSingleUniform(resource, guestBytes, collected, proof)) return false; const auto elementBytes = BytesPerElement(resource.format); if (elementBytes == 0 || elementBytes > sizeof(std::uint32_t) * 4) return false; std::uint32_t pattern[4]{}; @@ -2204,7 +2204,7 @@ void StorageTexture::upload(const std::vector* layers) { filledKeys = DccKeys::Uncompressed; VkClearColorValue clearValue{}; const bool fastClear = IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle; - if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, storageFormat, uploadedKeys, clearValue))) { + if (fastClear && (ClearColorFor(storageFormat, uploadedKeys, clearValue) || SingleKeyColorFor(descriptor, guestBytes, now, singleProof, storageFormat, uploadedKeys, clearValue))) { // A fast-cleared surface is cleared on the GPU; its texel memory is neither read nor filled. // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms From 275008a34154a265ebd970afbffd7551141c9048 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:26:17 +0100 Subject: [PATCH 04/22] test(agc): cover the uniform single-key DCC clear The ClearSingle case was only covered by a non-uniform surface, which exercises the CPU fallback, never the fast clear the proof enables. RunUniformSingleClear lays out a surface whose every 256-byte block starts with the same color, so ProvedSingleUniform holds and upload clears on the GPU instead of reading the guest texels. The write-back checks both halves of the contract: every written-back texel is either the kernel's write or the proved clear color (a wrong SingleKeyColorFor or FillClearColor value fails here), and on write-watched memory the upload keeps no guest snapshot, so a fallback that read the surface fails the snapshot check. Forcing ProvedSingleUniform to return false leaves the identical image content but a valid snapshot, which the watched run rejects. --- .../tests/execution/DccKeysFirstWrite.cpp | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index e9668e78dc..f90e7b9307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -47,6 +47,7 @@ struct VulkanDeviceTestAccess { namespace { using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::LookupOutcomes; using AgcDriver::Graphics::Require; using AgcDriver::Graphics::StorageTexture; using ShaderRecompiler::ShaderStage; @@ -451,6 +452,46 @@ void RunSingleKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(wrong == 0, message); } +void RunUniformSingleClear(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + const std::uint32_t color = 0x11223344u; + std::memset(texels, 0x55, SurfaceBytes); + for (std::size_t offset = 0; offset < SurfaceBytes; offset += 256) std::memcpy(texels + offset, &color, 4); + std::memset(keys, 0x10, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + const auto uploads = AgcDriver::Graphics::ThreadLookupOutcomes().counts; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "uniform single-key clear: the first write did not store the keys uncompressed"); + const auto& uploaded = AgcDriver::Graphics::ThreadLookupOutcomes().counts; + Require(uploaded[LookupOutcomes::UploadClear] == uploads[LookupOutcomes::UploadClear] + 1 && uploaded[LookupOutcomes::UploadCpu] == uploads[LookupOutcomes::UploadCpu], "uniform single-key clear: the first write read the surface and uploaded it instead of clearing it on the GPU"); + const bool stillWatched = AgcDriver::GuestMemory::Watched(surface, SurfaceBytes); + Require(image->GuestSnapshotValid() == !stillWatched, stillWatched ? "uniform single-key clear: the upload of write-watched memory kept a guest snapshot, so the surface was read rather than cleared" : "uniform single-key clear: the upload of untracked memory kept no guest bytes to compare"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test uniform single-key clear"); + device.WaitIdle(); + RequireKeys(keys, 0xff, "uniform single-key clear: the keys after the write-back"); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::size_t wrong = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + const auto offset = layout.Offset(x, y); + std::uint32_t texel = 0; + std::memcpy(&texel, texels + offset, 4); + const auto expected = y == 0 && x < Threads ? 0xffffffffu : color; + if (texel != expected) ++wrong; + } + } + char message[256]; + std::snprintf(message, sizeof(message), "uniform single-key clear: %zu of %u written-back texels are neither the kernel's write nor the proved clear color 0x%08x", wrong, Side * Side, color); + Require(wrong == 0, message); +} + void RunSingleKeysSampled(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock, const char* memory) { auto* texels = block; auto* keys = block + KeysOffset; @@ -783,6 +824,11 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { int main() { try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif std::optional block; std::optional watched; std::optional untracked; @@ -802,6 +848,9 @@ int main() { std::optional single; single.emplace(false); RunSingleKeys(*device, single->Data()); + std::optional uniformClear; + uniformClear.emplace(false); + RunUniformSingleClear(*device, uniformClear->Data()); std::optional sampled; sampled.emplace(false); std::optional sampledOutput; @@ -824,6 +873,9 @@ int main() { std::optional sampledWatched; sampledWatched.emplace(true); RunSingleKeysSampled(*device, sampledWatched->Data(), sampledOutput->Data(), "write-watched"); + std::optional uniformClearWatched; + uniformClearWatched.emplace(true); + RunUniformSingleClear(*device, uniformClearWatched->Data()); #ifdef _WIN32 pendingSingle.emplace(true, true); #else From 1f5ab519b3fe76270239119a818561a52b27b819 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 08:46:41 -0300 Subject: [PATCH 05/22] test(libs): keep non-ASCII temporary paths out of narrow Windows calls in guest tests Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFileLock.cpp | 10 ++++--- core/libs/tests/GuestHeapReportForGame.cpp | 6 +++- core/libs/tests/GuestSystemExit.cpp | 33 +++++++++++++--------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 3fb7ac7b71..8aab1a8f9c 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -53,7 +53,8 @@ static bool Acquires(int descriptor, int mode) { struct Fixture { static inline unsigned serial = 0; - std::string path = (std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin")).string(); + std::filesystem::path native = std::filesystem::temp_directory_path() / ("anyps5-flock-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(serial++) + ".bin"); + std::string path = native.string(); std::set descriptors; int source = Open(true); int other = Open(false); @@ -76,7 +77,7 @@ struct Fixture { } ~Fixture() { for (const int descriptor : descriptors) Require(close_nid_postfix(descriptor) == 0); - Require(std::remove(path.c_str()) == 0); + Require(std::filesystem::remove(native)); } }; @@ -262,7 +263,7 @@ static int ChildTry(const Fixture (&fixtures)[6], int mode) { wchar_t executable[32768]; Require(::GetModuleFileNameW(nullptr, executable, 32768) != 0); std::wstring command = L"\"" + std::wstring(executable) + L"\" child " + std::to_wstring(mode | 4); - for (const Fixture& fixture : fixtures) command += L" \"" + std::filesystem::path(fixture.path).wstring() + L"\""; + for (const Fixture& fixture : fixtures) command += L" \"" + fixture.native.filename().wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -300,7 +301,8 @@ int main(int argc, char** argv) { if (argc >= 4 && std::string(argv[1]) == "child") { int busy = 0; for (int index = 3; index < argc; ++index) { - const int descriptor = open_nid_postfix(argv[index], 2, 0); + const std::string path = (std::filesystem::temp_directory_path() / argv[index]).string(); + const int descriptor = open_nid_postfix(path.c_str(), 2, 0); Require(descriptor >= 0); if (flock_nid_postfix(descriptor, std::atoi(argv[2])) == 0) { Require(flock_nid_postfix(descriptor, 8) == 0); diff --git a/core/libs/tests/GuestHeapReportForGame.cpp b/core/libs/tests/GuestHeapReportForGame.cpp index 9238d7bdb1..de3ce98250 100644 --- a/core/libs/tests/GuestHeapReportForGame.cpp +++ b/core/libs/tests/GuestHeapReportForGame.cpp @@ -20,7 +20,11 @@ static void Require(bool value, const char* message) { int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-heap-report-" + std::to_string(std::rand()) + ".txt"); - Require(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr redirection failed"); +#ifdef _WIN32 + Require(_wfreopen(path.wstring().c_str(), L"w", stderr) != nullptr, "stderr redirection failed"); +#else + Require(std::freopen(path.c_str(), "w", stderr) != nullptr, "stderr redirection failed"); +#endif char heapName[] = "SceLibcHeap"; sceLibcInternalBacktraceForGame_nid_postfix(heapName); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 93ff93b094..1d8e6161cf 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -70,14 +70,24 @@ void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); #endif constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER"; constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE"; -void SetVariable(const char* name, const char* value) { +void SetVariable(const char* name, const std::filesystem::path& value) { #ifdef _WIN32 - Require(SetEnvironmentVariableA(name, value) != 0); - Require(_putenv_s(name, value) == 0); + const std::wstring wideName = std::filesystem::path(name).wstring(); + Require(SetEnvironmentVariableW(wideName.c_str(), value.c_str()) != 0); + Require(_wputenv_s(wideName.c_str(), value.c_str()) == 0); #else - Require(setenv(name, value, 1) == 0); + Require(setenv(name, value.c_str(), 1) == 0); #endif } +std::filesystem::path PathVariable(const char* name) { +#ifdef _WIN32 + const wchar_t* value = _wgetenv(std::filesystem::path(name).wstring().c_str()); +#else + const char* value = std::getenv(name); +#endif + Require(value != nullptr); + return value; +} std::filesystem::path OwnExecutable() { #ifdef _WIN32 std::wstring executable(32768, L'\0'); @@ -94,9 +104,7 @@ std::filesystem::path OwnExecutable() { #endif } void AppendMarker(const std::string& line) { - const char* marker = std::getenv(RelaunchMarker); - Require(marker != nullptr); - std::ofstream(marker, std::ios::app) << line << '\n'; + std::ofstream(PathVariable(RelaunchMarker), std::ios::app) << line << '\n'; } void MainShutdown() { AppendMarker("shutdown"); @@ -140,7 +148,7 @@ int RunChild(const std::filesystem::path& self, const char* mode) { int DriveNestedExit() { const auto self = OwnExecutable(); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-nested-exit-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); - SetVariable(RelaunchMarker, marker.string().c_str()); + SetVariable(RelaunchMarker, marker); const int code = RunChild(self, "--nested-exit"); std::string text; { @@ -157,7 +165,7 @@ int DriveNestedExit() { int DriveExitOrder() { const auto self = OwnExecutable(); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-order-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); - SetVariable(RelaunchMarker, marker.string().c_str()); + SetVariable(RelaunchMarker, marker); const int code = RunChild(self, "--exit-order"); std::string text; { @@ -176,8 +184,8 @@ int DriveRelaunch() { const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); std::filesystem::copy_file(self, secondary, std::filesystem::copy_options::overwrite_existing); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); - SetVariable(RelaunchMarker, marker.string().c_str()); - SetVariable(MainExecutable, self.string().c_str()); + SetVariable(RelaunchMarker, marker); + SetVariable(MainExecutable, self); Require(RunChild(self, "--relaunch") == 0); std::string text; for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { @@ -260,8 +268,7 @@ int main(int argc, char** argv) { return 2; } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunched") == 0) { - const char* main = std::getenv(MainExecutable); - Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main)); + Require(std::filesystem::equivalent(OwnExecutable(), PathVariable(MainExecutable))); AppendMarker("relaunched"); return 0; } From 89f829b5d8356783431796408cf27246422e422e Mon Sep 17 00:00:00 2001 From: janzezno Date: Wed, 21 Oct 2026 01:29:18 +0200 Subject: [PATCH 06/22] fix(agc): sync a host read on a store recorded after its snapshot check FlushForAccess checked the pending-write snapshot before it looked for overlapping pending and flushing images. A flusher moves an image the other way: pending, flushing, recorded into the open batch, landed. A reader descheduled after the snapshot check while the store was recorded and the image unregistered found nothing flushing and read guest memory before the recorded copy ran. Sync again when the snapshot overlaps the range after the image checks. --- core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9f03343a67..0e37bddcd8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1064,7 +1064,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } else { stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published); } - if (stored || published) { + if (stored || published || SnapshotOverlaps(address, bytes)) { // Stores into imported memory were only recorded; the CPU is about to read them. The wait // targets the batch holding them (the open one: SyncThrough submits it) and runs with the // mutex released when this acquisition is the outermost; a nested hook waits locked through From c44f3b59f741f50485b36af225639c6edcd6a547 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:44:07 +0100 Subject: [PATCH 07/22] feat(libkernel): implement pthread_timedjoin_np --- core/libs/prx/libkernel/CMakeLists.txt | 15 ++ .../prx/libkernel/Pthread/Posix/Thread.cpp | 7 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 24 ++- core/libs/tests/GuestTimedJoin.cpp | 183 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 + 5 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestTimedJoin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index bf78477ff7..41cf94083b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1869,6 +1869,21 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) + add_test_executable(guest_timed_join_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimedJoin.cpp) + target_include_directories(guest_timed_join_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_timed_join_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_timed_join_tests) + foreach(joinMode IN ITEMS normal coarse) + add_test(NAME guest_timed_join_${joinMode} COMMAND guest_timed_join_tests $) + set_tests_properties(guest_timed_join_${joinMode} PROPERTIES TIMEOUT 15) + if(joinMode STREQUAL coarse) + set_tests_properties(guest_timed_join_${joinMode} PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + endif() + if(WIN32) + set_tests_properties(guest_timed_join_${joinMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_sleep_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepCancel.cpp) target_include_directories(guest_sleep_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sleep_cancel_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index dc71c1cebc..e33d5d0b2d 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -18,6 +18,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadDetach(Pthread thread); void APS5_VABI scePthreadExit(void* retval); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI JoinThread_nid_no_patch(Pthread thread, void** retval, const KernelTimespec* abstime); int APS5_VABI scePthreadRename(Pthread thread, const char* name); int APS5_VABI scePthreadGetname(Pthread thread, char* name); Pthread APS5_VABI scePthreadSelf(); @@ -234,6 +235,12 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +int APS5_VABI pthread_timedjoin_np_nid_postfix(Pthread thread, void** value, const KernelTimespec* abstime) { + if (abstime == nullptr || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) + return PosixThread::GUEST_EINVAL; + return PosixThread::ToErrno(JoinThread_nid_no_patch(thread, value, abstime)); +} + namespace { struct RegisterPthreadOnLoad { RegisterPthreadOnLoad() { diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index bd61a61fc9..31b784005d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -2,6 +2,7 @@ #include "../include/ThreadLifecycle.hpp" #include "../include/WindowsThreadLocal.hpp" #include "../include/Cancel.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -926,7 +927,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread return SCE_OK; } -int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { +int APS5_VABI JoinThread_nid_no_patch(Pthread thread, void** retval, const KernelTimespec* abstime) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; @@ -938,7 +939,22 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { if (thread->_joining) return SCE_KERNEL_ERROR_EOPNOTSUPP; thread->_joining = true; while (!thread->_finished.load()) { - ThreadCancel::Wait(thread->_join_cv, lock, std::nullopt); + std::optional deadline; + if (abstime != nullptr) { + KernelTimespec now{}; + if (clock_gettime_nid_postfix(0, &now) != 0) { + thread->_joining = false; + throw std::runtime_error("Joining guest thread: realtime clock unavailable"); + } + if (abstime->tv_sec < now.tv_sec || (abstime->tv_sec == now.tv_sec && abstime->tv_nsec <= now.tv_nsec)) { + thread->_joining = false; + return SCE_KERNEL_ERROR_ETIMEDOUT; + } + const auto seconds = static_cast(abstime->tv_sec) - static_cast(now.tv_sec); + const auto nanos = seconds > 1 ? std::int64_t{1000000000} : static_cast(seconds) * 1000000000 + abstime->tv_nsec - now.tv_nsec; + deadline = TimedWait::DeadlineNanos(static_cast((nanos + 999) / 1000)); + } + ThreadCancel::Wait(thread->_join_cv, lock, deadline); if (ThreadCancel::Requested()) { thread->_joining = false; lock.unlock(); @@ -964,6 +980,10 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { return SCE_OK; } +int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { + return JoinThread_nid_no_patch(thread, retval, nullptr); +} + int APS5_VABI scePthreadDetach(Pthread thread) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; { diff --git a/core/libs/tests/GuestTimedJoin.cpp b/core/libs/tests/GuestTimedJoin.cpp new file mode 100644 index 0000000000..34ce27fdf0 --- /dev/null +++ b/core/libs/tests/GuestTimedJoin.cpp @@ -0,0 +1,183 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +struct Timespec { std::int64_t seconds, nanoseconds; }; +static_assert(sizeof(Timespec)==16 && alignof(Timespec)==8 && offsetof(Timespec,nanoseconds)==8); +extern "C" { +int APS5_VABI pthread_create_nid_postfix(void**,const void*,void* (APS5_VABI*)(void*),void*); +int APS5_VABI pthread_join_nid_postfix(void*,void**); +int APS5_VABI pthread_detach_nid_postfix(void*); +int APS5_VABI pthread_cancel_nid_postfix(void*); +void* APS5_VABI pthread_self_nid_postfix(); +int APS5_VABI clock_gettime_nid_postfix(int,Timespec*); +int* APS5_VABI __error_nid_postfix(); +} +using TimedJoin=int (APS5_VABI*)(void*,void**,const Timespec*); +static TimedJoin timedJoin; + +void Require(bool value,const char* message) { + if (!value) { std::fprintf(stderr,"timed join: %s\n",message); std::abort(); } +} + +Timespec After(int milliseconds) { + Timespec result{}; + Require(clock_gettime_nid_postfix(0,&result)==0,"read realtime"); + result.nanoseconds+=static_cast(milliseconds)*1000000; + result.seconds+=result.nanoseconds/1000000000; + result.nanoseconds%=1000000000; + return result; +} + +struct Gate { std::atomic release{false}, entered{false}, leaving{false}; }; +void* APS5_VABI Worker(void* argument) { + auto& gate=*static_cast(argument); + gate.entered.store(true,std::memory_order_release); + while (!gate.release.load(std::memory_order_acquire)) std::this_thread::yield(); + gate.leaving.store(true,std::memory_order_release); + return reinterpret_cast(std::uintptr_t{0x12345678}); +} + +void Wait(const std::atomic& flag) { + const auto deadline=std::chrono::steady_clock::now()+std::chrono::seconds(3); + while (!flag.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() entered{false}; int result=-1; void* value=reinterpret_cast(std::uintptr_t{0x55}); }; +void* APS5_VABI Joiner(void* argument) { + auto& task=*static_cast(argument); + const auto deadline=After(3000); + task.entered.store(true,std::memory_order_release); + task.result=timedJoin(task.target,&task.value,&deadline); + return nullptr; +} + +void AwaitClaim(void* target) { + const Timespec past{0,0}; + const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(2); + for (;;) { + const int result=timedJoin(target,nullptr,&past); + if (result==45) return; + Require(result==60,"claim probe returns timeout or ENOTSUP"); + Require(std::chrono::steady_clock::now()(std::uintptr_t{0x55}); std::uint64_t after=0x5a5a5a5a; } result; + const Timespec past{0,0}; + *__error_nid_postfix()=123; + Require(timedJoin(thread,&result.value,&past)==60,"past deadline expires"); + Require(result.value==reinterpret_cast(std::uintptr_t{0x55}),"timeout preserves return slot"); + Require(*__error_nid_postfix()==123,"POSIX error preserves errno"); + const auto deadline=After(50); + const auto originalDeadline=deadline; + const auto started=std::chrono::steady_clock::now(); + Require(timedJoin(thread,&result.value,&deadline)==60,"future deadline expires"); + Require(std::chrono::steady_clock::now()-started>=std::chrono::milliseconds(25),"wait did not expire immediately"); + Require(deadline.seconds==originalDeadline.seconds && deadline.nanoseconds==originalDeadline.nanoseconds,"deadline input is unchanged"); + gate.release.store(true,std::memory_order_release); + const Timespec distant{std::numeric_limits::max(),999999999}; + Require(timedJoin(thread,&result.value,&distant)==0,"timeout leaves target joinable; distant deadline does not overflow"); + Require(result.value==reinterpret_cast(std::uintptr_t{0x12345678}),"joined return value"); + Require(result.before==0xa5a5a5a5 && result.after==0x5a5a5a5a,"return-slot bounds"); + Require(timedJoin(thread,nullptr,&valid)==3,"already joined target"); + Require(timedJoin(thread,nullptr,nullptr)==22,"deadline validation precedes joined state"); + Gate done; + void* completed=Start(done); + done.release.store(true,std::memory_order_release); + Wait(done.leaving); + const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(2); + for (;;) { + const int joined=timedJoin(completed,nullptr,&past); + if (joined==0) break; + Require(joined==60 && std::chrono::steady_clock::now()(std::uintptr_t{1}),"joiner is canceled"); + Require(task.result==-1 && task.value==reinterpret_cast(std::uintptr_t{0x55}),"canceled join did not return or write result"); + const Timespec past{0,0}; + Require(timedJoin(target,nullptr,&past)==60,"cancellation releases join claim"); + gate.release.store(true,std::memory_order_release); + const auto deadline=After(1000); + Require(timedJoin(target,nullptr,&deadline)==0,"cancellation preserves target joinability"); + } else { + gate.release.store(true,std::memory_order_release); + Require(pthread_join_nid_postfix(joiner,nullptr)==0,"join successful joiner"); + Require(task.result==0 && task.value==reinterpret_cast(std::uintptr_t{0x12345678}),"competing join succeeds once"); + } +} + +int main(int argc,char** argv) { + Require(argc==2,"expected kernel path"); +#ifdef _WIN32 + const auto library=LoadLibraryA(argv[1]); + Require(library!=nullptr,"load kernel"); + timedJoin=reinterpret_cast(GetProcAddress(library,"pthread_timedjoin_np_nid_postfix")); +#else + const auto library=dlopen(argv[1],RTLD_NOW); + Require(library!=nullptr,"load kernel"); + timedJoin=reinterpret_cast(dlsym(library,"pthread_timedjoin_np_nid_postfix")); +#endif + Require(timedJoin!=nullptr,"pthread_timedjoin_np export is missing"); + Basic(); + Contention(false); + Contention(true); +#ifdef _WIN32 + Require(FreeLibrary(library)!=0,"release kernel lookup handle"); +#else + Require(dlclose(library)==0,"release kernel lookup handle"); +#endif + std::puts("timed join checks passed"); +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2883d5c3d4..765760fd8f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -113,6 +113,8 @@ - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [pthread_timedjoin_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel, NID `PkS44IGrDkM`) - the signature follows [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/pthread_np.h#L71); deadline validation, absolute realtime deadlines, error precedence and join ownership follow [FreeBSD libthr](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_join.c). The [public 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c) lists this NID, but PS5 behavior and module/library versions have not been measured. Realtime is rechecked at most two seconds apart while waiting. After guest cleanup completes, final native thread teardown uses the existing unbounded join and may finish after the deadline; host teardown is not bounded by this implementation. + ### Functional - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling From 1fcfacb27e238f53614171a2cdfb9249ab4a481f Mon Sep 17 00:00:00 2001 From: janzezno Date: Wed, 21 Oct 2026 16:05:49 +0200 Subject: [PATCH 08/22] fix(agc): keep completion write-backs off the full sync in the recorded-store recheck --- core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 0e37bddcd8..ea9e30be81 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1064,7 +1064,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } else { stored = StorageTexture::FlushPending(address, bytes, nullptr, "memory access", scope, &published); } - if (stored || published || SnapshotOverlaps(address, bytes)) { + if (stored || published || (completionDepth == 0 && SnapshotOverlaps(address, bytes))) { // Stores into imported memory were only recorded; the CPU is about to read them. The wait // targets the batch holding them (the open one: SyncThrough submits it) and runs with the // mutex released when this acquisition is the outermost; a nested hook waits locked through From bbd9ee6d2c3aa3e537fa6359f95e5d104a1d3167 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 19:02:53 +0100 Subject: [PATCH 09/22] feat(libkernel_sys): implement SIGEV_NONE POSIX timers --- core/libs/CMakeLists.txt | 11 + .../prx/libkernel_sys/Export.Discovery.cpp | 25 --- core/libs/prx/libkernel_sys/Export.cpp | 152 +++++++++++++ core/libs/tests/GuestKernelSysKtimer.cpp | 204 ++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 1 + 5 files changed, 368 insertions(+), 25 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysKtimer.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..e5aad2009f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -611,6 +611,17 @@ if(BUILD_TESTING) configure_windows_unwind(savedata_replace_file_failure_tests) add_test(NAME savedata_replace_file_failure COMMAND savedata_replace_file_failure_tests) + add_test_executable(guest_kernel_sys_ktimer_tests tests/GuestKernelSysKtimer.cpp) + target_include_directories(guest_kernel_sys_ktimer_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_ktimer_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_ktimer_tests) + add_test(NAME guest_kernel_sys_ktimer COMMAND guest_kernel_sys_ktimer_tests) + set_tests_properties(guest_kernel_sys_ktimer PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_ktimer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_crt_tests tests/GuestCrt.cpp) target_include_directories(guest_crt_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_crt_tests PRIVATE libSceAmpr libc) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..fb6c8e3cea 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -278,31 +278,6 @@ int APS5_VABI ksem_wait_nid_postfix(void) { return 0; } -int APS5_VABI ktimer_create_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_delete_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_getoverrun_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_gettime_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ktimer_settime_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ktrace_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 9c7d6fc2b6..0ac387a960 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -691,3 +692,154 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const } } + +extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp); + +namespace { + +constexpr int ClockRealtime = 0; +constexpr int ClockMonotonic = 4; +constexpr int SigevNone = 0; +constexpr int SigevSignal = 1; +constexpr int SigevThreadId = 4; +constexpr int SignalMax = 128; +constexpr int TimerAbstime = 1; +constexpr int FirstTimer = 3; +constexpr int TimerMax = 32; +constexpr long long NanosecondsPerSecond = 1000000000; +constexpr long long TickNanoseconds = 1000000; + +struct GuestSigevent { + int notify; + int signo; +}; + +struct GuestTimespec { + std::int64_t tv_sec; + std::int64_t tv_nsec; +}; + +struct GuestItimerspec { + GuestTimespec it_interval; + GuestTimespec it_value; +}; + +using Nanoseconds = __int128; + +struct Ktimer { + bool live = false; + int clock = ClockRealtime; + Nanoseconds value = 0; + Nanoseconds interval = 0; +}; + +std::mutex g_ktimerLock; +Ktimer g_ktimers[TimerMax]; + +Nanoseconds ToNanoseconds(const GuestTimespec& time) { + return static_cast(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec; +} + +GuestTimespec ToTimespec(Nanoseconds time) { + return {static_cast(time / NanosecondsPerSecond), + static_cast(time % NanosecondsPerSecond)}; +} + +Nanoseconds ClockNow(int clock) { + GuestTimespec now{}; + clock_gettime_nid_postfix(clock, &now); + return ToNanoseconds(now); +} + +bool FixTimespec(GuestTimespec& time) { + if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false; + if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds; + return true; +} + +Ktimer* FindTimer(int id) { + if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr; + return &g_ktimers[id]; +} + +void Expire(Ktimer& timer, Nanoseconds now) { + if (timer.value == 0 || now < timer.value) return; + if (timer.interval == 0) { + timer.value = 0; + return; + } + timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval; +} + +GuestItimerspec Remaining(Ktimer& timer) { + const Nanoseconds now = ClockNow(timer.clock); + Expire(timer, now); + GuestItimerspec result{ToTimespec(timer.interval), {}}; + if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1}; + return result; +} + +} + +extern "C" { + +int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) { + if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast(Fail(GuestEinval)); + if (event != nullptr) { + if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId) + return static_cast(Fail(GuestEinval)); + if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax)) + return static_cast(Fail(GuestEinval)); + } + std::lock_guard lock(g_ktimerLock); + int id = FirstTimer; + while (id < TimerMax && g_ktimers[id].live) ++id; + if (id == TimerMax) return static_cast(Fail(GuestEagain)); + if (timerid == nullptr) return static_cast(Fail(GuestEfault)); + if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal"); + g_ktimers[id] = {true, clock, 0, 0}; + *timerid = id; + return 0; +} + +int APS5_VABI ktimer_delete_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast(Fail(GuestEinval)); + *timer = {}; + return 0; +} + +int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { + if (value == nullptr) return static_cast(Fail(GuestEfault)); + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast(Fail(GuestEinval)); + GuestItimerspec next = *value; + if (!FixTimespec(next.it_value)) return static_cast(Fail(GuestEinval)); + const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0; + if (armed && !FixTimespec(next.it_interval)) return static_cast(Fail(GuestEinval)); + if (!armed) next.it_interval = {}; + if (oldValue != nullptr) *oldValue = Remaining(*timer); + timer->interval = ToNanoseconds(next.it_interval); + timer->value = ToNanoseconds(next.it_value); + if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock); + return 0; +} + +int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast(Fail(GuestEinval)); + if (value == nullptr) return static_cast(Fail(GuestEfault)); + *value = Remaining(*timer); + return 0; +} + +int APS5_VABI ktimer_getoverrun_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + if (FindTimer(id) == nullptr) return static_cast(Fail(GuestEinval)); + return 0; +} + +} diff --git a/core/libs/tests/GuestKernelSysKtimer.cpp b/core/libs/tests/GuestKernelSysKtimer.cpp new file mode 100644 index 0000000000..2dc1cc763f --- /dev/null +++ b/core/libs/tests/GuestKernelSysKtimer.cpp @@ -0,0 +1,204 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Sigevent { + int notify; + int signo; + std::int64_t value; + std::int64_t extra[6]; +}; + +struct Timespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +struct Itimerspec { + Timespec interval; + Timespec value; +}; + +} + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI clock_gettime_nid_postfix(int clock, Timespec* time); +int APS5_VABI ktimer_create_nid_postfix(int clock, const Sigevent* event, int* timerid); +int APS5_VABI ktimer_delete_nid_postfix(int timerid); +int APS5_VABI ktimer_settime_nid_postfix(int timerid, int flags, const Itimerspec* value, Itimerspec* oldValue); +int APS5_VABI ktimer_gettime_nid_postfix(int timerid, Itimerspec* value); +int APS5_VABI ktimer_getoverrun_nid_postfix(int timerid); +} + +namespace { + +constexpr int Efault = 14; +constexpr int Einval = 22; +constexpr int Eagain = 35; +constexpr int ClockRealtime = 0; +constexpr int ClockMonotonic = 4; +constexpr int ClockUptime = 5; +constexpr int TimerAbstime = 1; +constexpr std::int64_t Millisecond = 1000000; +constexpr std::int64_t Second = 1000000000; + +const Sigevent None{0, 0, 0, {}}; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +std::int64_t Nanoseconds(const Timespec& time) { + return time.seconds * Second + time.nanoseconds; +} + +int Create(int clock = ClockMonotonic) { + int id = -1; + Require(ktimer_create_nid_postfix(clock, &None, &id) == 0); + return id; +} + +Itimerspec Get(int id) { + Itimerspec value{{-1, -1}, {-1, -1}}; + Require(ktimer_gettime_nid_postfix(id, &value) == 0); + return value; +} + +void Sleep(std::int64_t nanoseconds) { + std::this_thread::sleep_for(std::chrono::nanoseconds(nanoseconds)); +} + +void Creation() { + int id = -1; + const Sigevent badNotify{2, 0, 0, {}}; + const Sigevent noSignal{1, 0, 0, {}}; + const Sigevent badSignal{4, 129, 0, {}}; + Require(ktimer_create_nid_postfix(ClockUptime, &None, &id) == -1 && Error() == Einval && id == -1); + Require(ktimer_create_nid_postfix(-1, &None, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &badNotify, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &noSignal, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &badSignal, &id) == -1 && Error() == Einval); + Require(ktimer_create_nid_postfix(ClockMonotonic, &None, nullptr) == -1 && Error() == Efault); + + std::vector ids; + for (int expected = 3; expected < 32; ++expected) { + ids.push_back(Create(expected % 2 == 0 ? ClockRealtime : ClockMonotonic)); + Require(ids.back() == expected); + } + Require(ktimer_create_nid_postfix(ClockMonotonic, &None, &id) == -1 && Error() == Eagain); + Require(ktimer_delete_nid_postfix(7) == 0); + Require(Create() == 7); + for (const int each : ids) Require(ktimer_delete_nid_postfix(each) == 0); +} + +void InvalidIds() { + Itimerspec value{}; + const int id = Create(); + Require(ktimer_delete_nid_postfix(id) == 0); + for (const int bad : {-1, 0, 2, id, 32}) { + Require(ktimer_delete_nid_postfix(bad) == -1 && Error() == Einval); + Require(ktimer_settime_nid_postfix(bad, 0, &value, nullptr) == -1 && Error() == Einval); + Require(ktimer_gettime_nid_postfix(bad, &value) == -1 && Error() == Einval); + Require(ktimer_getoverrun_nid_postfix(bad) == -1 && Error() == Einval); + } + Require(ktimer_settime_nid_postfix(id, 0, nullptr, nullptr) == -1 && Error() == Efault); +} + +void Values() { + const int id = Create(); + Itimerspec disarmed = Get(id); + Require(Nanoseconds(disarmed.value) == 0 && Nanoseconds(disarmed.interval) == 0); + for (const Itimerspec bad : {Itimerspec{{0, 0}, {-1, 0}}, Itimerspec{{0, 0}, {0, -1}}, Itimerspec{{0, 0}, {0, Second}}, + Itimerspec{{-1, 0}, {1, 0}}, Itimerspec{{0, Second}, {1, 0}}}) + Require(ktimer_settime_nid_postfix(id, 0, &bad, nullptr) == -1 && Error() == Einval); + + const Itimerspec clear{{-1, 0}, {0, 0}}; + Require(ktimer_settime_nid_postfix(id, 0, &clear, nullptr) == 0); + disarmed = Get(id); + Require(Nanoseconds(disarmed.value) == 0 && Nanoseconds(disarmed.interval) == 0); + + const Itimerspec small{{0, 1}, {10, 0}}; + Require(ktimer_settime_nid_postfix(id, 0, &small, nullptr) == 0); + Itimerspec old{}; + const Itimerspec later{{0, 0}, {20, 0}}; + Require(ktimer_settime_nid_postfix(id, 0, &later, &old) == 0); + Require(Nanoseconds(old.interval) == Millisecond); + Require(Nanoseconds(old.value) > 9 * Second && Nanoseconds(old.value) <= 10 * Second); + const Itimerspec tiny{{0, 0}, {0, 1}}; + Require(ktimer_settime_nid_postfix(id, 0, &tiny, &old) == 0); + Require(Nanoseconds(old.value) > 19 * Second && Nanoseconds(old.value) <= 20 * Second); + const Itimerspec off{}; + Require(ktimer_settime_nid_postfix(id, 0, &off, &old) == 0); + Require(Nanoseconds(old.interval) == 0 && Nanoseconds(old.value) <= Millisecond); + Require(ktimer_delete_nid_postfix(id) == 0); +} + +void Expiry() { + const int id = Create(); + const Itimerspec once{{0, 0}, {0, 30 * Millisecond}}; + Require(ktimer_settime_nid_postfix(id, 0, &once, nullptr) == 0); + const auto running = Get(id); + Require(Nanoseconds(running.value) > 0 && Nanoseconds(running.value) <= 30 * Millisecond); + Sleep(60 * Millisecond); + const auto expired = Get(id); + Require(Nanoseconds(expired.value) == 0 && Nanoseconds(expired.interval) == 0); + Require(ktimer_getoverrun_nid_postfix(id) == 0); + + const Itimerspec periodic{{0, 10 * Millisecond}, {0, 10 * Millisecond}}; + Require(ktimer_settime_nid_postfix(id, 0, &periodic, nullptr) == 0); + Sleep(55 * Millisecond); + const auto period = Get(id); + Require(Nanoseconds(period.interval) == 10 * Millisecond); + Require(Nanoseconds(period.value) > 0 && Nanoseconds(period.value) <= 10 * Millisecond); + Require(ktimer_getoverrun_nid_postfix(id) == 0); + Require(ktimer_delete_nid_postfix(id) == 0); +} + +void Absolute() { + for (const int clock : {ClockRealtime, ClockMonotonic}) { + const int id = Create(clock); + Timespec now{}; + Require(clock_gettime_nid_postfix(clock, &now) == 0); + const Itimerspec future{{0, 0}, {now.seconds + 5, now.nanoseconds}}; + Require(ktimer_settime_nid_postfix(id, TimerAbstime, &future, nullptr) == 0); + const auto remaining = Get(id); + Require(Nanoseconds(remaining.value) > 4 * Second && Nanoseconds(remaining.value) <= 5 * Second); + + const Itimerspec past{{0, 0}, {now.seconds - 1, now.nanoseconds}}; + Require(ktimer_settime_nid_postfix(id, TimerAbstime, &past, nullptr) == 0); + Require(Nanoseconds(Get(id).value) == 0); + const Itimerspec pastPeriodic{{1, 0}, {now.seconds - 10, now.nanoseconds}}; + Require(ktimer_settime_nid_postfix(id, TimerAbstime, &pastPeriodic, nullptr) == 0); + const auto moved = Get(id); + Require(Nanoseconds(moved.value) > 0 && Nanoseconds(moved.value) <= Second); + Require(Nanoseconds(moved.interval) == Second); + Require(ktimer_delete_nid_postfix(id) == 0); + } +} + +} + +int main() { + Creation(); + InvalidIds(); + Values(); + Expiry(); + Absolute(); + std::puts("guest_kernel_sys_ktimer passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..1bceba3297 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -7,6 +7,7 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified +- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified From 7820a5353a09607a252c32a1e20520ad392d889f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 22:22:16 +0100 Subject: [PATCH 10/22] refactor(libkernel): move the ktimer calls into libkernel The PS5 3.20 export lists have ktimer_* in libkernel as well as libkernel_sys, and on Windows a libkernel import never reaches libkernel_sys. The implementation moves unchanged to libkernel/Time/Ktimer.cpp. --- core/libs/prx/libkernel/CMakeLists.txt | 1 + core/libs/prx/libkernel/Time/Ktimer.cpp | 165 ++++++++++++++++++++++++ core/libs/prx/libkernel_sys/Export.cpp | 152 ---------------------- docs/dev/debt/libkernel.md | 1 + docs/dev/debt/libkernel_sys.md | 1 - 5 files changed, 167 insertions(+), 153 deletions(-) create mode 100644 core/libs/prx/libkernel/Time/Ktimer.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..2785bbe6e5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -74,6 +74,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/Ktimer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp diff --git a/core/libs/prx/libkernel/Time/Ktimer.cpp b/core/libs/prx/libkernel/Time/Ktimer.cpp new file mode 100644 index 0000000000..6dd8b16800 --- /dev/null +++ b/core/libs/prx/libkernel/Time/Ktimer.cpp @@ -0,0 +1,165 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp); + +namespace { + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEagain = 35; + +std::int64_t Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +constexpr int ClockRealtime = 0; +constexpr int ClockMonotonic = 4; +constexpr int SigevNone = 0; +constexpr int SigevSignal = 1; +constexpr int SigevThreadId = 4; +constexpr int SignalMax = 128; +constexpr int TimerAbstime = 1; +constexpr int FirstTimer = 3; +constexpr int TimerMax = 32; +constexpr long long NanosecondsPerSecond = 1000000000; +constexpr long long TickNanoseconds = 1000000; + +struct GuestSigevent { + int notify; + int signo; +}; + +struct GuestTimespec { + std::int64_t tv_sec; + std::int64_t tv_nsec; +}; + +struct GuestItimerspec { + GuestTimespec it_interval; + GuestTimespec it_value; +}; + +using Nanoseconds = __int128; + +struct Ktimer { + bool live = false; + int clock = ClockRealtime; + Nanoseconds value = 0; + Nanoseconds interval = 0; +}; + +std::mutex g_ktimerLock; +Ktimer g_ktimers[TimerMax]; + +Nanoseconds ToNanoseconds(const GuestTimespec& time) { + return static_cast(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec; +} + +GuestTimespec ToTimespec(Nanoseconds time) { + return {static_cast(time / NanosecondsPerSecond), + static_cast(time % NanosecondsPerSecond)}; +} + +Nanoseconds ClockNow(int clock) { + GuestTimespec now{}; + clock_gettime_nid_postfix(clock, &now); + return ToNanoseconds(now); +} + +bool FixTimespec(GuestTimespec& time) { + if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false; + if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds; + return true; +} + +Ktimer* FindTimer(int id) { + if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr; + return &g_ktimers[id]; +} + +void Expire(Ktimer& timer, Nanoseconds now) { + if (timer.value == 0 || now < timer.value) return; + if (timer.interval == 0) { + timer.value = 0; + return; + } + timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval; +} + +GuestItimerspec Remaining(Ktimer& timer) { + const Nanoseconds now = ClockNow(timer.clock); + Expire(timer, now); + GuestItimerspec result{ToTimespec(timer.interval), {}}; + if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1}; + return result; +} + +} + +extern "C" { + +int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) { + if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast(Fail(GuestEinval)); + if (event != nullptr) { + if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId) + return static_cast(Fail(GuestEinval)); + if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax)) + return static_cast(Fail(GuestEinval)); + } + std::lock_guard lock(g_ktimerLock); + int id = FirstTimer; + while (id < TimerMax && g_ktimers[id].live) ++id; + if (id == TimerMax) return static_cast(Fail(GuestEagain)); + if (timerid == nullptr) return static_cast(Fail(GuestEfault)); + if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal"); + g_ktimers[id] = {true, clock, 0, 0}; + *timerid = id; + return 0; +} + +int APS5_VABI ktimer_delete_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast(Fail(GuestEinval)); + *timer = {}; + return 0; +} + +int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { + if (value == nullptr) return static_cast(Fail(GuestEfault)); + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast(Fail(GuestEinval)); + GuestItimerspec next = *value; + if (!FixTimespec(next.it_value)) return static_cast(Fail(GuestEinval)); + const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0; + if (armed && !FixTimespec(next.it_interval)) return static_cast(Fail(GuestEinval)); + if (!armed) next.it_interval = {}; + if (oldValue != nullptr) *oldValue = Remaining(*timer); + timer->interval = ToNanoseconds(next.it_interval); + timer->value = ToNanoseconds(next.it_value); + if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock); + return 0; +} + +int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) { + std::lock_guard lock(g_ktimerLock); + Ktimer* timer = FindTimer(id); + if (timer == nullptr) return static_cast(Fail(GuestEinval)); + if (value == nullptr) return static_cast(Fail(GuestEfault)); + *value = Remaining(*timer); + return 0; +} + +int APS5_VABI ktimer_getoverrun_nid_postfix(int id) { + std::lock_guard lock(g_ktimerLock); + if (FindTimer(id) == nullptr) return static_cast(Fail(GuestEinval)); + return 0; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 0ac387a960..9c7d6fc2b6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -692,154 +691,3 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const } } - -extern "C" int APS5_VABI clock_gettime_nid_postfix(int clockId, void* tp); - -namespace { - -constexpr int ClockRealtime = 0; -constexpr int ClockMonotonic = 4; -constexpr int SigevNone = 0; -constexpr int SigevSignal = 1; -constexpr int SigevThreadId = 4; -constexpr int SignalMax = 128; -constexpr int TimerAbstime = 1; -constexpr int FirstTimer = 3; -constexpr int TimerMax = 32; -constexpr long long NanosecondsPerSecond = 1000000000; -constexpr long long TickNanoseconds = 1000000; - -struct GuestSigevent { - int notify; - int signo; -}; - -struct GuestTimespec { - std::int64_t tv_sec; - std::int64_t tv_nsec; -}; - -struct GuestItimerspec { - GuestTimespec it_interval; - GuestTimespec it_value; -}; - -using Nanoseconds = __int128; - -struct Ktimer { - bool live = false; - int clock = ClockRealtime; - Nanoseconds value = 0; - Nanoseconds interval = 0; -}; - -std::mutex g_ktimerLock; -Ktimer g_ktimers[TimerMax]; - -Nanoseconds ToNanoseconds(const GuestTimespec& time) { - return static_cast(time.tv_sec) * NanosecondsPerSecond + time.tv_nsec; -} - -GuestTimespec ToTimespec(Nanoseconds time) { - return {static_cast(time / NanosecondsPerSecond), - static_cast(time % NanosecondsPerSecond)}; -} - -Nanoseconds ClockNow(int clock) { - GuestTimespec now{}; - clock_gettime_nid_postfix(clock, &now); - return ToNanoseconds(now); -} - -bool FixTimespec(GuestTimespec& time) { - if (time.tv_sec < 0 || time.tv_nsec < 0 || time.tv_nsec >= NanosecondsPerSecond) return false; - if (time.tv_sec == 0 && time.tv_nsec != 0 && time.tv_nsec < TickNanoseconds) time.tv_nsec = TickNanoseconds; - return true; -} - -Ktimer* FindTimer(int id) { - if (id < FirstTimer || id >= TimerMax || !g_ktimers[id].live) return nullptr; - return &g_ktimers[id]; -} - -void Expire(Ktimer& timer, Nanoseconds now) { - if (timer.value == 0 || now < timer.value) return; - if (timer.interval == 0) { - timer.value = 0; - return; - } - timer.value += ((now - timer.value) / timer.interval + 1) * timer.interval; -} - -GuestItimerspec Remaining(Ktimer& timer) { - const Nanoseconds now = ClockNow(timer.clock); - Expire(timer, now); - GuestItimerspec result{ToTimespec(timer.interval), {}}; - if (timer.value != 0) result.it_value = timer.value > now ? ToTimespec(timer.value - now) : GuestTimespec{0, 1}; - return result; -} - -} - -extern "C" { - -int APS5_VABI ktimer_create_nid_postfix(int clock, const GuestSigevent* event, int* timerid) { - if (clock != ClockRealtime && clock != ClockMonotonic) return static_cast(Fail(GuestEinval)); - if (event != nullptr) { - if (event->notify != SigevNone && event->notify != SigevSignal && event->notify != SigevThreadId) - return static_cast(Fail(GuestEinval)); - if (event->notify != SigevNone && (event->signo <= 0 || event->signo > SignalMax)) - return static_cast(Fail(GuestEinval)); - } - std::lock_guard lock(g_ktimerLock); - int id = FirstTimer; - while (id < TimerMax && g_ktimers[id].live) ++id; - if (id == TimerMax) return static_cast(Fail(GuestEagain)); - if (timerid == nullptr) return static_cast(Fail(GuestEfault)); - if (event == nullptr || event->notify != SigevNone) NotImplemented_nid_no_patch("ktimer_create delivering a signal"); - g_ktimers[id] = {true, clock, 0, 0}; - *timerid = id; - return 0; -} - -int APS5_VABI ktimer_delete_nid_postfix(int id) { - std::lock_guard lock(g_ktimerLock); - Ktimer* timer = FindTimer(id); - if (timer == nullptr) return static_cast(Fail(GuestEinval)); - *timer = {}; - return 0; -} - -int APS5_VABI ktimer_settime_nid_postfix(int id, int flags, const GuestItimerspec* value, GuestItimerspec* oldValue) { - if (value == nullptr) return static_cast(Fail(GuestEfault)); - std::lock_guard lock(g_ktimerLock); - Ktimer* timer = FindTimer(id); - if (timer == nullptr) return static_cast(Fail(GuestEinval)); - GuestItimerspec next = *value; - if (!FixTimespec(next.it_value)) return static_cast(Fail(GuestEinval)); - const bool armed = next.it_value.tv_sec != 0 || next.it_value.tv_nsec != 0; - if (armed && !FixTimespec(next.it_interval)) return static_cast(Fail(GuestEinval)); - if (!armed) next.it_interval = {}; - if (oldValue != nullptr) *oldValue = Remaining(*timer); - timer->interval = ToNanoseconds(next.it_interval); - timer->value = ToNanoseconds(next.it_value); - if (armed && (flags & TimerAbstime) == 0) timer->value += ClockNow(timer->clock); - return 0; -} - -int APS5_VABI ktimer_gettime_nid_postfix(int id, GuestItimerspec* value) { - std::lock_guard lock(g_ktimerLock); - Ktimer* timer = FindTimer(id); - if (timer == nullptr) return static_cast(Fail(GuestEinval)); - if (value == nullptr) return static_cast(Fail(GuestEfault)); - *value = Remaining(*timer); - return 0; -} - -int APS5_VABI ktimer_getoverrun_nid_postfix(int id) { - std::lock_guard lock(g_ktimerLock); - if (FindTimer(id) == nullptr) return static_cast(Fail(GuestEinval)); - return 0; -} - -} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..e5afde5a0a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -13,6 +13,7 @@ ### Unknown function info - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. +- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 1bceba3297..25499ae3d1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -7,7 +7,6 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. A directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified -- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified From 642c0d56ac9f7112ee60f56c7a055c3b577ee362 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 22:27:24 +0100 Subject: [PATCH 11/22] docs(debt): list the ktimer calls under Functional --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e5afde5a0a..f970d5bfb2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -13,7 +13,6 @@ ### Unknown function info - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. -- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. @@ -147,6 +146,7 @@ - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store +- [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them From f53d2d74c2c67637b12afe9e157055fa010ac63b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:20:29 +0300 Subject: [PATCH 12/22] feat(libSceGnmDriverForNeoMode): write the Neo default hardware state --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 34 +++++--- .../include/InitSequences.hpp | 77 +++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 9b7f2684c4..37159bb113 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -1,7 +1,26 @@ #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp" + +namespace { + +constexpr std::uint32_t InitPacketWords = 0x100; + +template +int WriteHardwareState(std::uint32_t* cmdbuf, std::uint32_t size, const std::array& state) { + if (size < InitPacketWords) return 0; + auto* at = std::copy(GnmNeo::ClearState.begin(), GnmNeo::ClearState.end(), cmdbuf); + at = std::copy(state.begin(), state.end(), at); + const auto padding = static_cast(cmdbuf + InitPacketWords - at - 1); + at[0] = 0xc0001000u | ((padding - 1) << 16); + at[1] = 0; + return InitPacketWords; +} + +} // namespace namespace { @@ -116,19 +135,16 @@ int APS5_VABI sceGnmDrawIndirectMulti(void) { return 0; } -int APS5_VABI sceGnmDrawInitDefaultHardwareState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitDefaultHardwareState(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState); } -int APS5_VABI sceGnmDrawInitDefaultHardwareState200(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState200); } -int APS5_VABI sceGnmDrawInitDefaultHardwareState350(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState350); } int APS5_VABI sceGnmDrawInitToDefaultContextState(void) { diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp new file mode 100644 index 0000000000..4ba1654296 --- /dev/null +++ b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef CORE_LIBS_PRX_LIBSCEGNMDRIVERFORNEOMODE_INCLUDE_INITSEQUENCES_HPP +#define CORE_LIBS_PRX_LIBSCEGNMDRIVERFORNEOMODE_INCLUDE_INITSEQUENCES_HPP + +#include +#include + +namespace GnmNeo { + +constexpr std::array ClearState{ + 0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0055800u, 0x2ec47fc0u, 0xffffffffu, + 0x0u, 0x0u, 0x0u, 0xau, +}; + +constexpr std::array HardwareState{ + 0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x215u, + 0x0u, 0xc0016900u, 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, + 0x280u, 0x80008u, 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, + 0xc0016900u, 0x206u, 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, + 0x10u, 0xc0016900u, 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, + 0x2fbu, 0x3f800000u, 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, + 0xc0016900u, 0x30eu, 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, + 0xc0017600u, 0x7u, 0x1ffu, 0xc0017600u, 0x46u, 0x1ffu, 0xc0017600u, 0x87u, + 0x1ffu, 0xc0017600u, 0xc7u, 0x1ffu, 0xc0017600u, 0x107u, 0x0u, 0xc0017600u, + 0x147u, 0x1ffu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, 0x0u, + 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, 0x284u, + 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, 0xc0016900u, + 0x292u, 0x0u, 0xc0016900u, 0x293u, 0x6000000u, 0xc0016900u, 0x2f8u, 0x0u, + 0xc0016900u, 0x2deu, 0x1e9u, 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, + 0xc0017900u, 0x200u, 0xe0000000u, +}; + +constexpr std::array HardwareState200{ + 0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x219u, + 0xffffffffu, 0xc0017600u, 0x21au, 0xffffffffu, 0xc0017600u, 0x215u, 0x0u, 0xc0016900u, + 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, + 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, + 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, + 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, + 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, + 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0017600u, 0x7u, + 0x1701ffu, 0xc0017600u, 0x46u, 0x1701fdu, 0xc0017600u, 0x87u, 0x1701ffu, 0xc0017600u, + 0xc7u, 0x1701fdu, 0xc0017600u, 0x107u, 0x17u, 0xc0017600u, 0x147u, 0x1701fdu, + 0xc0017600u, 0x47u, 0x1cu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, + 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, + 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, + 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, 0x293u, 0x6020000u, 0xc0016900u, 0x2f8u, + 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, 0xc0026900u, 0xebu, 0xff00ff00u, 0xff00u, + 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, 0xc0017900u, 0x200u, 0xe0000000u, + 0xc0017900u, 0x40000258u, 0x6d007fu, +}; + +constexpr std::array HardwareState350{ + 0xc0017600u, 0x216u, 0xffffffffu, 0xc0017600u, 0x217u, 0xffffffffu, 0xc0017600u, 0x219u, + 0xffffffffu, 0xc0017600u, 0x21au, 0xffffffffu, 0xc0017600u, 0x215u, 0x0u, 0xc0016900u, + 0x2f9u, 0x2du, 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, + 0xc0016900u, 0x281u, 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, + 0x43fu, 0xc0016900u, 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, + 0x2fau, 0x3f800000u, 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, + 0xc0016900u, 0x2fdu, 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, + 0xffffffffu, 0xc0016900u, 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0017600u, 0x7u, + 0x1701ffu, 0xc0017600u, 0x46u, 0x1701fdu, 0xc0017600u, 0x87u, 0x1701ffu, 0xc0017600u, + 0xc7u, 0x1701fdu, 0xc0017600u, 0x107u, 0x17u, 0xc0017600u, 0x147u, 0x1701fdu, + 0xc0017600u, 0x47u, 0x1cu, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, 0x101u, + 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, 0xc0016900u, + 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, 0x0u, + 0xc0016900u, 0x102u, 0x0u, 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, 0x293u, + 0x6020000u, 0xc0016900u, 0x2f8u, 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, 0xc0026900u, + 0xebu, 0xff00ff00u, 0xff00u, 0xc0036900u, 0x295u, 0x100u, 0x100u, 0x4u, + 0xc0017900u, 0x200u, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu, +}; + +} // namespace GnmNeo + +#endif From aa61cdc839218fb0590c233f9ad6fdcecff42729 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:20:29 +0300 Subject: [PATCH 13/22] feat(libSceGnmDriverForNeoMode): write the Neo default context state --- .../prx/libSceGnmDriverForNeoMode/Export.cpp | 19 ++++++++-------- .../include/InitSequences.hpp | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp index 37159bb113..7b715efbb1 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -20,9 +20,12 @@ int WriteHardwareState(std::uint32_t* cmdbuf, std::uint32_t size, const std::arr return InitPacketWords; } -} // namespace - -namespace { +template +int WriteContextState(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t packetWords, const std::array& state) { + if (size != packetWords) return 0; + std::copy(state.begin(), state.end(), cmdbuf); + return static_cast(packetWords); +} constexpr int Failure = static_cast(0x8EEE00FFu); constexpr int NoInterface = static_cast(0x80000000u); @@ -147,14 +150,12 @@ int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t* cmdbuf, std:: return WriteHardwareState(cmdbuf, size, GnmNeo::HardwareState350); } -int APS5_VABI sceGnmDrawInitToDefaultContextState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteContextState(cmdbuf, size, 0x20, GnmNeo::ContextState); } -int APS5_VABI sceGnmDrawInitToDefaultContextState400(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t* cmdbuf, std::uint32_t size) { + return WriteContextState(cmdbuf, size, 0x100, GnmNeo::ContextState400); } int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void) { diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp index 4ba1654296..5768dd1c92 100644 --- a/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp +++ b/core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp @@ -72,6 +72,28 @@ constexpr std::array HardwareState350{ 0xc0017900u, 0x200u, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu, }; +constexpr std::array ContextState{ + 0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0002f00u, 0x1u, 0xc0016900u, + 0x102u, 0x0u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0026900u, 0xebu, 0xff00ff00u, + 0xff00u, 0xc00d1000u, 0x0u, +}; + +constexpr std::array ContextState400{ + 0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0x0u, 0xc0016900u, 0x2f9u, 0x2du, + 0xc0016900u, 0x282u, 0x8u, 0xc0016900u, 0x280u, 0x80008u, 0xc0016900u, 0x281u, + 0xffff0000u, 0xc0016900u, 0x204u, 0x0u, 0xc0016900u, 0x206u, 0x43fu, 0xc0016900u, + 0x83u, 0xffffu, 0xc0016900u, 0x317u, 0x10u, 0xc0016900u, 0x2fau, 0x3f800000u, + 0xc0016900u, 0x2fcu, 0x3f800000u, 0xc0016900u, 0x2fbu, 0x3f800000u, 0xc0016900u, 0x2fdu, + 0x3f800000u, 0xc0016900u, 0x202u, 0xcc0010u, 0xc0016900u, 0x30eu, 0xffffffffu, 0xc0016900u, + 0x30fu, 0xffffffffu, 0xc0002f00u, 0x1u, 0xc0016900u, 0x1b1u, 0x2u, 0xc0016900u, + 0x101u, 0x0u, 0xc0016900u, 0x100u, 0xffffffffu, 0xc0016900u, 0x103u, 0x0u, + 0xc0016900u, 0x284u, 0x0u, 0xc0016900u, 0x290u, 0x0u, 0xc0016900u, 0x2aeu, + 0x0u, 0xc0016900u, 0x102u, 0x0u, 0xc0016900u, 0x292u, 0x0u, 0xc0016900u, + 0x293u, 0x6020000u, 0xc0016900u, 0x2f8u, 0x0u, 0xc0016900u, 0x2deu, 0x1e9u, + 0xc0026900u, 0xebu, 0xff00ff00u, 0xff00u, 0xc0036900u, 0x295u, 0x100u, 0x100u, + 0x4u, 0xc0017900u, 0x40000258u, 0x6d007fu, 0xc09a1000u, 0x0u, +}; + } // namespace GnmNeo #endif From 1caaded886dd2602d6122bd3cb8a987479ea8b8b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:21:05 +0300 Subject: [PATCH 14/22] test(libSceGnmDriverForNeoMode): check the default hardware and context state packets --- core/libs/CMakeLists.txt | 10 +++ core/libs/tests/GuestGnmNeoDefaultState.cpp | 76 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/libs/tests/GuestGnmNeoDefaultState.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..cf86fab599 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -720,4 +720,14 @@ if(BUILD_TESTING) set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_gnm_neo_default_state_tests tests/GuestGnmNeoDefaultState.cpp) + target_include_directories(guest_gnm_neo_default_state_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_gnm_neo_default_state_tests PRIVATE libSceGnmDriverForNeoMode libc) + configure_windows_unwind(guest_gnm_neo_default_state_tests) + add_test(NAME guest_gnm_neo_default_state COMMAND guest_gnm_neo_default_state_tests) + if(WIN32) + set_tests_properties(guest_gnm_neo_default_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/tests/GuestGnmNeoDefaultState.cpp b/core/libs/tests/GuestGnmNeoDefaultState.cpp new file mode 100644 index 0000000000..7288e795c4 --- /dev/null +++ b/core/libs/tests/GuestGnmNeoDefaultState.cpp @@ -0,0 +1,76 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceGnmDrawInitDefaultHardwareState(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitDefaultHardwareState200(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitDefaultHardwareState350(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitToDefaultContextState(std::uint32_t*, std::uint32_t); +int APS5_VABI sceGnmDrawInitToDefaultContextState400(std::uint32_t*, std::uint32_t); +} + +namespace { + +using StateWriter = int (APS5_VABI *)(std::uint32_t*, std::uint32_t); + +constexpr std::uint32_t Untouched = 0xa5a5a5a5u; +constexpr std::uint32_t BufferWords = 0x110; + +std::uint32_t g_words[BufferWords]; + +void Require(bool value) { if (!value) std::abort(); } + +std::uint32_t* Fresh() { + for (auto& word : g_words) word = Untouched; + return g_words; +} + +void RequireAt(std::uint32_t at, std::initializer_list expected) { + for (const auto word : expected) Require(g_words[at++] == word); +} + +void RequireUntouchedFrom(std::uint32_t at) { + for (; at < BufferWords; ++at) Require(g_words[at] == Untouched); +} + +void CheckHardwareState(StateWriter writer, std::uint32_t stateWords, std::initializer_list tail) { + Require(writer(Fresh(), 0xff) == 0); + RequireUntouchedFrom(0); + + Require(writer(Fresh(), 0x100) == 0x100); + RequireAt(0, {0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0, 0xc0055800u, 0x2ec47fc0u, 0xffffffffu, 0, 0, 0, 10}); + RequireAt(12, {0xc0017600u, 0x216, 0xffffffffu, 0xc0017600u, 0x217, 0xffffffffu}); + const std::uint32_t end = 12 + stateWords; + RequireAt(end - static_cast(tail.size()), tail); + const std::uint32_t padding = 0x100 - end - 1; + RequireAt(end, {0xc0001000u | ((padding - 1) << 16), 0}); + for (std::uint32_t at = end + 2; at < 0x100; ++at) Require(g_words[at] == Untouched); + RequireUntouchedFrom(0x100); + + Require(writer(Fresh(), 0x10f) == 0x100); + RequireUntouchedFrom(0x100); +} + +void CheckContextState(StateWriter writer, std::uint32_t packetWords, std::uint32_t stateWords, std::initializer_list tail) { + Require(writer(Fresh(), packetWords - 1) == 0); + RequireUntouchedFrom(0); + Require(writer(Fresh(), packetWords + 1) == 0); + RequireUntouchedFrom(0); + + Require(writer(Fresh(), packetWords) == static_cast(packetWords)); + RequireAt(0, {0xc0012800u, 0x80000000u, 0x80000000u, 0xc0001200u, 0}); + RequireAt(stateWords - static_cast(tail.size()), tail); + RequireUntouchedFrom(stateWords); +} + +} // namespace + +int main() { + CheckHardwareState(sceGnmDrawInitDefaultHardwareState, 115, {0x100, 4, 0xc0017900u, 0x200, 0xe0000000u}); + CheckHardwareState(sceGnmDrawInitDefaultHardwareState200, 131, {0x200, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu}); + CheckHardwareState(sceGnmDrawInitDefaultHardwareState350, 134, {0x200, 0xe0000000u, 0xc0017900u, 0x40000258u, 0x6d007fu}); + CheckContextState(sceGnmDrawInitToDefaultContextState, 0x20, 19, {0xff00ff00u, 0xff00, 0xc00d1000u, 0}); + CheckContextState(sceGnmDrawInitToDefaultContextState400, 0x100, 102, {0x40000258u, 0x6d007fu, 0xc09a1000u, 0}); +} From f6d5447d54828e0ed1af667c787c4ab3de0ea00d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 18:21:05 +0300 Subject: [PATCH 15/22] docs(debt): record the libSceGnmDriverForNeoMode default state packets --- docs/dev/debt/libSceGnmDriverForNeoMode.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md index 46f92c14e9..44ebce9512 100644 --- a/docs/dev/debt/libSceGnmDriverForNeoMode.md +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -2,8 +2,9 @@ ### Unknown function info -- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 70 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 65 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read +- [default state packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmDrawInitDefaultHardwareState`, `sceGnmDrawInitDefaultHardwareState200`, `sceGnmDrawInitDefaultHardwareState350`, `sceGnmDrawInitToDefaultContextState` and `sceGnmDrawInitToDefaultContextState400` write the sequences in [InitSequences.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp), taken from [shadPS4's gnmdriver_init.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver_init.h): its Neo sequences, except the plain hardware state, which shadPS4 also writes unchanged in Neo mode. The size checks and returned sizes are shadPS4's. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet From 235ea5fd2260c6327eab43a82f08fd70c2236925 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:37:19 +0100 Subject: [PATCH 16/22] fix(libkernel): remove duplicate timed join discovery export --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 702eff446b..83217edc19 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -518,11 +518,6 @@ int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_timedjoin_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ptrace_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; From b553950efd79fa645fc311ddc679d801826b6f4e Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:17:40 +0400 Subject: [PATCH 17/22] fix(ngs2): correct UserFx ids and preserve the Delay compatibility path --- .../libSceNgs2.native/include/Ngs2Types.hpp | 15 +- .../libs/prx/libSceNgs2.native/src/Custom.cpp | 26 ++- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 161 +++++++++++++++--- docs/dev/debt/libSceNgs2.native.md | 6 +- 4 files changed, 177 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 8358917c7b..c1102877b5 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -77,7 +77,8 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x400 static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN = 0x40001500; -static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001d00; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001900; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_DELAY = 0x40001d00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; @@ -91,7 +92,8 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE = 8; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_GAIN = 0x15; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_REVERB = 0x1b; -static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x1d; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x19; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_DELAY = 0x1d; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT = 0x20; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION = 0x23; @@ -206,6 +208,15 @@ struct Ngs2CustomModuleOption { std::uint32_t size; }; +struct Ngs2CustomDelayModuleOption { + Ngs2CustomModuleOption custom_module_option; + std::uint32_t type; + std::uint32_t max_taps; + float max_length; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2CustomDelayModuleOption) == 20); + struct Ngs2UserFx2SetupContext { void* common; void* param; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index aaad9fdb18..865a844cd7 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -30,7 +30,7 @@ static std::uint32_t OptionWord(const Ngs2CustomModuleOption& option) { static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { const auto stateSizeIs = [&info](std::uint32_t size) { return info.state_size == 0 || info.state_size == size; }; - if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN || info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_REVERB) { if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; @@ -45,6 +45,21 @@ static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { return SCE_NGS2_OK; } +static void CheckDelayModule(std::uint32_t rackId, const Ngs2CustomRackModuleInfo& info) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || info.state_size != 0 || info.state_offset != 0) { + throw std::runtime_error("NGS2: this custom Delay rack or module state is not implemented"); + } + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomDelayModuleOption)) { + throw std::runtime_error("NGS2: this custom Delay module option size is not implemented"); + } + Ngs2CustomDelayModuleOption delay{}; + std::memcpy(&delay, info.option, sizeof(delay)); + if (delay.type > 1 || delay.max_taps == 0 || delay.max_taps > 8 || + !(delay.max_length >= 1.0f && delay.max_length <= 20000.0f) || delay.reserved != 0) { + throw std::runtime_error("NGS2: these custom Delay module option values are not implemented"); + } +} + int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs) { if (option.num_buffers == 0 || option.num_buffers > SCE_NGS2_CUSTOM_MAX_BUFFERS || option.num_buffers < maxInputs) { return SCE_NGS2_ERROR_INVALID_NUM_BUFFERS; @@ -58,7 +73,10 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { - if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX; + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: the custom UserFx module on a custom sampler rack is not implemented"); + if (info.option != nullptr) throw std::runtime_error("NGS2: a custom UserFx module option is not implemented"); + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) { + CheckDelayModule(rackId, info); } else if (BuiltInModuleName(info.module_id) == nullptr) { throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); } @@ -186,6 +204,9 @@ static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std:: void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { const auto rackId = voice.rack->rackId; + if ((param.id & ~0xffu) == SCE_NGS2_CUSTOM_VOICE_PARAM_DELAY) { + throw std::runtime_error("NGS2: the custom Delay module parameters are not implemented"); + } if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); SetupCustomSubmixer(voice, ParamAs(param)); @@ -242,6 +263,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) { + if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) continue; if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { ProcessGain(voice, voice.customGain[m], channels, grain); } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 5b6acd7ad2..bb72a549bb 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include struct Calls { @@ -434,6 +436,7 @@ static void TestCustomRackChecks() { option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS; Require(CreateThrows(option)); + Require(create(BuiltInRackOption(userFx2, 0x19, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); Require(!CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); const ModuleOptionWords gainOption{sizeof(Ngs2CustomModuleOption), {}}; @@ -468,7 +471,7 @@ static void TestCustomRackChecks() { option.custom_rack_option.module[1] = good.custom_rack_option.module[0]; option.custom_rack_option.module[1].dest_buffer_id = 1; Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); - Require(CreateThrows(BuiltInRackOption(userFx2, 0x19, nullptr, 0))); + Require(CreateThrows(BuiltInRackOption(userFx2, 0x1d, nullptr, 0))); option = good; option.custom_rack_option.num_buffers = 0; @@ -500,6 +503,29 @@ static void TestCustomRackChecks() { Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; Require(CreateSamplerThrows(sampler)); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + for (const auto count : {1u, 2u, 24u}) { + sampler.custom_rack_option.num_modules = count; + for (std::uint32_t index = 0; index < count; ++index) { + sampler.custom_rack_option.module[index].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + } + sampler.custom_rack_option.module[count - 1].module_id = 0x19; + Require(CreateSamplerThrows(sampler)); + for (const bool useAllocator : {false, true}) { + rack = 0; + rejected = false; + try { + if (useAllocator) { + sceNgs2RackCreateWithAllocator(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &allocator, &rack); + } else { + sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack); + } + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && rack == 0 && allocCalls == 0); + } + } Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -512,6 +538,8 @@ struct LegacyUserFxState { }; static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) { + Require(context->user_data1 == 123 && context->user_data2 == 456); + Require(context->num_channels == 2 && context->num_grain_samples == Grain && context->sample_rate == 48000); auto& state = *reinterpret_cast(context->user_data0); state.calls++; state.flags = context->flags; @@ -523,7 +551,7 @@ static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) { return SCE_NGS2_OK; } -static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option) { +static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option, std::uint32_t moduleIndex) { Ngs2CustomSubmixerRackOption rack{}; auto& common = rack.custom_rack_option.rack_option; common.size = sizeof(rack); @@ -532,27 +560,28 @@ static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOptio common.max_matrices = 1; common.max_ports = 1; rack.custom_rack_option.num_buffers = 1; - rack.custom_rack_option.num_modules = 1; - rack.custom_rack_option.module[0].option = option; - rack.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; - rack.custom_rack_option.module[0].state_size = 16; + rack.custom_rack_option.num_modules = moduleIndex + 1; + for (std::uint32_t index = 0; index <= moduleIndex; ++index) { + rack.custom_rack_option.module[index].option = option; + rack.custom_rack_option.module[index].module_id = 0x19; + } rack.max_channels = 2; rack.max_inputs = 1; return rack; } -static void TestCustomLegacyUserFx() { +static void TestCustomLegacyUserFx(std::uint32_t moduleIndex) { const auto system = CreateSystem(); const auto master = Mastering(system, 2); const Ngs2CustomModuleOption header{sizeof(Ngs2CustomModuleOption)}; - const auto rack = CustomRack(system, LegacyRackOption(&header)); + const auto rack = CustomRack(system, LegacyRackOption(nullptr, moduleIndex)); const auto custom = Voice(rack); Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); Patch(custom, master); Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); LegacyUserFxState state{}; - Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Control(custom, 0x40001900 | moduleIndex, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state), 123, 456}); auto out = Render(system); @@ -568,20 +597,11 @@ static void TestCustomLegacyUserFx() { std::uint8_t dummy[10]; }; Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, BadSizeParam{})); - Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 2, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 31, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0})); - Ngs2CustomModuleOption badSize{2}; - Require(CreateThrows(LegacyRackOption(&badSize))); - - struct ExtendedOption { - std::uint32_t size = 20; - std::uint32_t reserved = 0; - std::uint32_t channels = 4; - float param0 = 1000.0f; - float param1 = 0.0f; - } extendedOption; - const auto rackExtended = CustomRack(system, LegacyRackOption(reinterpret_cast(&extendedOption))); - Require(sceNgs2RackDestroy(rackExtended, nullptr) == SCE_NGS2_OK); + Require(CreateThrows(LegacyRackOption(&header, moduleIndex))); + Require(ControlThrows(custom, 0x40001d00, + Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state), 123, 456})); state.result = -1; bool failed = false; @@ -595,7 +615,7 @@ static void TestCustomLegacyUserFx() { Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); - const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr)); + const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr, moduleIndex)); const auto voiceNullOpt = Voice(rackNullOpt); Control(voiceNullOpt, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); Patch(voiceNullOpt, master); @@ -604,7 +624,7 @@ static void TestCustomLegacyUserFx() { LegacyUserFxState state2{}; state2.val0 = 0.125f; state2.val1 = 0.75f; - Control(voiceNullOpt, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Control(voiceNullOpt, 0x40001900 | moduleIndex, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state2), 123, 456}); out = Render(system); Require(state2.calls == 1 && state2.flags == 1); @@ -614,6 +634,92 @@ static void TestCustomLegacyUserFx() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +struct DelayOption { + std::uint32_t size = 20; + std::uint32_t type = 0; + std::uint32_t maxTaps = 4; + float maxLength = 1000.0f; + std::uint32_t reserved = 0; +}; + +static Ngs2CustomSubmixerRackOption DelayRackOption(const DelayOption& delay, std::uint32_t count) { + auto option = LegacyRackOption(nullptr, count - 1); + option.custom_rack_option.rack_option.max_ports = 8; + for (std::uint32_t index = 0; index < count; ++index) { + option.custom_rack_option.module[index].module_id = 0x1d; + option.custom_rack_option.module[index].option = reinterpret_cast(&delay); + } + return option; +} + +static void TestCustomDelay(std::uint32_t count, const DelayOption& delay) { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + auto option = DelayRackOption(delay, count); + const auto rack = CustomRack(system, option); + const auto custom = Voice(rack); + Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(custom, master); + Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); + for (const float sample : Render(system)) Require(sample == 0.0f); + + std::vector pcm(Grain * 2 * 8); + for (std::uint32_t i = 0; i < pcm.size() / 2; ++i) { + pcm[i * 2] = static_cast(1000 + i * 17); + pcm[i * 2 + 1] = static_cast(-2000 + static_cast(i) * 11); + } + const auto sampler = Sampler(system, pcm); + Patch(sampler, custom); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto checkGrain = [&](std::uint32_t grain) { + const auto out = Render(system); + for (std::uint32_t i = 0; i < Grain * 2; ++i) { + Require(out[i] == static_cast(pcm[grain * Grain * 2 + i]) / 32768.0f); + } + }; + for (std::uint32_t grain = 0; grain < 3; ++grain) checkGrain(grain); + + LegacyUserFxState state{}; + for (std::uint32_t index = 0; index < 256; ++index) { + Ngs2CustomVoiceUserFxParam param{{sizeof(Ngs2CustomVoiceUserFxParam), 0, 0x40001d00 | index}, + ProcessLegacy, reinterpret_cast(&state), 123, 456}; + bool rejected = false; + try { + sceNgs2VoiceControl(custom, ¶m.header); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + rejected = message.find("Delay") != std::string::npos && message.find("not implemented") != std::string::npos; + } + Require(rejected); + } + checkGrain(3); + Require(state.calls == 0); + Event(sampler, SCE_NGS2_VOICE_EVENT_STOP); + for (const float sample : Render(system)) Require(sample == 0.0f); + + option.custom_rack_option.module[0].state_size = 4; + Require(CreateThrows(option)); + option.custom_rack_option.module[0].state_size = 0; + option.custom_rack_option.module[0].state_offset = 4; + Require(CreateThrows(option)); + option.custom_rack_option.module[0].state_offset = 0; + option.custom_rack_option.module[0].option = nullptr; + Require(CreateThrows(option)); + Ngs2CustomSamplerRackOption samplerOption{}; + samplerOption.custom_rack_option = DelayRackOption(delay, count).custom_rack_option; + samplerOption.custom_rack_option.rack_option.size = sizeof(samplerOption); + samplerOption.max_channel_works = 1; + Require(CreateSamplerThrows(samplerOption)); + for (const auto invalid : {DelayOption{19}, DelayOption{24}, DelayOption{20, 2}, DelayOption{20, 0, 0}, + DelayOption{20, 0, 9}, DelayOption{20, 0, 4, 0.0f}, DelayOption{20, 0, 4, 20001.0f}, + DelayOption{20, 0, 4, std::numeric_limits::quiet_NaN()}, + DelayOption{20, 0, 4, std::numeric_limits::infinity()}, DelayOption{20, 0, 4, 1000.0f, 1}}) { + Require(CreateThrows(DelayRackOption(invalid, count))); + } + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestCustomGain() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -664,12 +770,17 @@ static void TestCustomGain() { } int main() { + for (const auto count : {1u, 2u, 24u}) { + for (const auto delay : {DelayOption{}, DelayOption{20, 1, 1, 1.0f, 0}, DelayOption{20, 1, 8, 20000.0f, 0}}) { + TestCustomDelay(count, delay); + } + } TestUserFxChain(); TestRackOptions(); TestCustomRackChecks(); TestCustomSampler(); TestCustomFilter(); - TestCustomLegacyUserFx(); + for (const auto moduleIndex : {0u, 1u, 23u}) TestCustomLegacyUserFx(moduleIndex); TestCustomGain(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 37afdd32a1..57b0113258 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -19,8 +19,10 @@ - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules and Gain on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input -- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed, nor Gain (0x15) on a custom sampler rack: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - the 48-byte process context, process flags, callback error handling and null-handler behavior retain the existing submixer callback model and are not console-verified. Non-null module options throw because their console error is unknown. UserFx on custom sampler rack 0x4001 throws: its parameter indexing and routing are not implemented. Module id 0x19, the 40-byte voice parameter (handler and three user-data words), parameter id `0x40001900 | index`, and lack of option and module state follow [fma-rc's PS5 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943), including parameter dispatch at 0x25ec4. +- [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) - [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it - [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts From 721a2e4b734160a48945407c786df9b07357325d Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 20 Oct 2026 23:52:10 +0100 Subject: [PATCH 18/22] fix(opus): decode at the requested rate with bundled libopus --- .gitmodules | 3 + 3rdparty/opus | 1 + CMakeLists.txt | 1 + cmake/Opus.cmake | 17 +++ .../libs/prx/libSceOpusCeltDec/CMakeLists.txt | 4 +- core/libs/prx/libSceOpusDec/CMakeLists.txt | 4 +- .../prx/libSceOpusDec/include/OpusDecoder.hpp | 100 +++--------------- core/libs/tests/GuestOpusCeltDec.cpp | 36 +++++-- core/libs/tests/GuestOpusDec.cpp | 47 ++++++++ docs/dev/debt/libSceOpusCeltDec.md | 2 +- docs/dev/debt/libSceOpusDec.md | 4 +- tools/package_release.py | 2 + tools/tests/test_package_release.py | 19 ++++ 13 files changed, 136 insertions(+), 104 deletions(-) create mode 160000 3rdparty/opus create mode 100644 cmake/Opus.cmake diff --git a/.gitmodules b/.gitmodules index 64a98a2f94..d867f24e45 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,9 @@ [submodule "3rdparty/ffmpeg-core"] path = 3rdparty/ffmpeg-core url = https://github.com/KytyPS5/ext-ffmpeg-core.git +[submodule "3rdparty/opus"] + path = 3rdparty/opus + url = https://github.com/xiph/opus.git [submodule "3rdparty/freetype"] path = 3rdparty/freetype url = https://github.com/freetype/freetype.git diff --git a/3rdparty/opus b/3rdparty/opus new file mode 160000 index 0000000000..22244de5a7 --- /dev/null +++ b/3rdparty/opus @@ -0,0 +1 @@ +Subproject commit 22244de5a79bd1d6d623c32e72bf1954b56235be diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eaa66c8db..9136b46104 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,6 +347,7 @@ add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL) set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON) include(cmake/ZArchive.cmake) +include(cmake/Opus.cmake) if(WIN32) set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE) set(MI_BUILD_STATIC OFF CACHE BOOL "" FORCE) diff --git a/cmake/Opus.cmake b/cmake/Opus.cmake new file mode 100644 index 0000000000..4f655afc78 --- /dev/null +++ b/cmake/Opus.cmake @@ -0,0 +1,17 @@ +function(add_bundled_opus) + set(BUILD_SHARED_LIBS OFF) + set(BUILD_TESTING OFF) + set(OPUS_BUILD_SHARED_LIBRARY OFF) + set(OPUS_BUILD_TESTING OFF) + set(OPUS_BUILD_PROGRAMS OFF) + set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF) + set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF) + add_subdirectory(3rdparty/opus EXCLUDE_FROM_ALL) + configure_file(3rdparty/opus/COPYING ${CMAKE_BINARY_DIR}/core/libs/licenses/COPYING.opus COPYONLY) + set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON) + if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a) + endif() +endfunction() + +add_bundled_opus() diff --git a/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt index 32be01d2dd..38a71d09b5 100644 --- a/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt +++ b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceOpusCeltDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceOpusCeltDec PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceOpusCeltDec PRIVATE ffmpeg) +target_link_libraries(libSceOpusCeltDec PRIVATE opus) configure_windows_unwind(libSceOpusCeltDec) if(BUILD_TESTING) add_test_executable(guest_opus_celt_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusCeltDec.cpp) target_include_directories(guest_opus_celt_dec_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc) + target_link_libraries(guest_opus_celt_dec_tests PRIVATE libSceOpusCeltDec libc opus) configure_windows_unwind(guest_opus_celt_dec_tests) add_test(NAME guest_opus_celt_dec COMMAND guest_opus_celt_dec_tests) if(WIN32) diff --git a/core/libs/prx/libSceOpusDec/CMakeLists.txt b/core/libs/prx/libSceOpusDec/CMakeLists.txt index 4179bce7ff..01bf008dd3 100644 --- a/core/libs/prx/libSceOpusDec/CMakeLists.txt +++ b/core/libs/prx/libSceOpusDec/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceOpusDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceOpusDec PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceOpusDec PRIVATE ffmpeg) +target_link_libraries(libSceOpusDec PRIVATE opus) configure_windows_unwind(libSceOpusDec) if(BUILD_TESTING) add_test_executable(guest_opus_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusDec.cpp) target_include_directories(guest_opus_dec_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc) + target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc opus) configure_windows_unwind(guest_opus_dec_tests) add_test(NAME guest_opus_dec COMMAND guest_opus_dec_tests) if(WIN32) diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp index fee4c48957..fa9cf66b31 100644 --- a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -1,12 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP #define CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP -#include -#include #include #include #include -#include #include #include #include @@ -15,10 +12,7 @@ #include #include -extern "C" { -#include -#include -} +#include namespace { @@ -65,20 +59,10 @@ public: decoder->context = *context; decoder->channels = channels; decoder->sampleRate = sampleRate; - const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); - if (!codec) fail("FFmpeg Opus decoder unavailable"); - decoder->codec = avcodec_alloc_context3(codec); - if (!decoder->codec) throw std::bad_alloc(); - av_channel_layout_default(&decoder->codec->ch_layout, channels); - decoder->codec->sample_rate = 48000; - if (avcodec_open2(decoder->codec, codec, nullptr) < 0) fail("cannot open FFmpeg decoder"); - if (sampleRate != 48000) { - AVChannelLayout layout{}; - av_channel_layout_default(&layout, channels); - if (swr_alloc_set_opts2(&decoder->resampler, &layout, AV_SAMPLE_FMT_FLT, sampleRate, &layout, AV_SAMPLE_FMT_FLT, 48000, 0, nullptr) < 0 || - swr_init(decoder->resampler) < 0) - fail("cannot open FFmpeg resampler"); - } + int error = OPUS_OK; + decoder->codec.reset(opus_decoder_create(sampleRate, channels, &error)); + if (error == OPUS_ALLOC_FAIL) throw std::bad_alloc(); + if (!decoder->codec || error != OPUS_OK) fail("cannot create libopus decoder"); decoders.emplace(state, std::move(decoder)); return 0; } @@ -92,49 +76,18 @@ public: std::lock_guard lock(mutex); const auto found = decoders.find(state); if (found == decoders.end()) return error(1); - if (bytes == 0 || bytes > std::numeric_limits::max() - AV_INPUT_BUFFER_PADDING_SIZE) fail("packet loss concealment or oversized packet is not implemented"); + if (bytes == 0) fail("packet loss concealment is not implemented"); auto& decoder = *found->second; if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); - const int samples = packetSamples(data, bytes) / (48000 / decoder.sampleRate); + const int samples = opus_packet_get_nb_samples(data, bytes, decoder.sampleRate); + if (samples <= 0) fail("invalid packet duration"); const int outputBytes = samples * decoder.channels * 2; - if (capacity / 2 * 2 < outputBytes) fail("PCM buffer is too small"); - std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); - std::unique_ptr frame(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); - if (!packet || !frame || av_new_packet(packet.get(), bytes) < 0) throw std::bad_alloc(); - std::memcpy(packet->data, data, bytes); - if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0) - fail("FFmpeg rejected the packet"); - const auto format = static_cast(frame->format); - if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || - (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) - fail("unexpected FFmpeg PCM format"); - std::vector decoded; - decoded.reserve(static_cast(frame->nb_samples) * decoder.channels); - for (int sample = 0; sample < frame->nb_samples; ++sample) { - for (int channel = 0; channel < decoder.channels; ++channel) { - const auto* source = reinterpret_cast(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - decoded.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); - } - } - if (decoder.resampler) { - const int capacitySamples = swr_get_out_samples(decoder.resampler, frame->nb_samples); - if (capacitySamples < 0) fail("FFmpeg resampler rejected the frame"); - std::vector resampled(static_cast(capacitySamples) * decoder.channels); - auto* output = reinterpret_cast(resampled.data()); - const auto* input = reinterpret_cast(decoded.data()); - const int converted = swr_convert(decoder.resampler, &output, capacitySamples, &input, frame->nb_samples); - if (converted < 0) fail("FFmpeg resampler rejected the frame"); - resampled.resize(static_cast(converted) * decoder.channels); - decoded = std::move(resampled); - } - decoder.pending.insert(decoder.pending.end(), decoded.begin(), decoded.end()); - const auto count = static_cast(samples) * decoder.channels; - if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); - for (std::size_t index = 0; index < count; ++index) { - const auto converted = static_cast(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f))); - std::memcpy(reinterpret_cast(pcm) + index * 2, &converted, 2); - } - decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast(count)); + if (capacity < outputBytes) fail("PCM buffer is too small"); + std::vector decoded(static_cast(samples) * decoder.channels); + const int result = opus_decode(decoder.codec.get(), data, bytes, decoded.data(), samples, 0); + if (result < 0) fail("libopus rejected the packet"); + if (result != samples) fail("unexpected libopus PCM duration"); + std::memcpy(pcm, decoded.data(), static_cast(outputBytes)); return outputBytes; } @@ -150,13 +103,7 @@ private: std::uint32_t context; int channels; int sampleRate; - AVCodecContext* codec = nullptr; - SwrContext* resampler = nullptr; - std::vector pending; - ~Decoder() { - swr_free(&resampler); - avcodec_free_context(&codec); - } + std::unique_ptr codec{nullptr, opus_decoder_destroy}; }; const char* name; @@ -173,23 +120,6 @@ private: [[noreturn]] void fail(const char* message) const { throw std::runtime_error(std::string(name) + ": " + message); } - - int packetSamples(const std::uint8_t* packet, int size) const { - const unsigned config = packet[0] >> 3; - const int frameSamples = config < 12 ? ((config & 3) == 3 ? 2880 : 480 << (config & 3)) - : config < 16 ? 480 << (config & 1) : 120 << (config & 3); - int frames = 1; - switch (packet[0] & 3) { - case 1: - case 2: frames = 2; break; - case 3: - if (size < 2) fail("truncated packet frame count"); - frames = packet[1] & 63; - break; - } - if (frames == 0 || frames * frameSamples > 5760) fail("invalid packet duration"); - return frames * frameSamples; - } }; } // namespace diff --git a/core/libs/tests/GuestOpusCeltDec.cpp b/core/libs/tests/GuestOpusCeltDec.cpp index 3245cc824e..b0dc8ae383 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include extern "C" { int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*); @@ -128,18 +130,30 @@ int main() { for (int rate : {0, 22050, 44100, 96000}) Require(sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast(errorBase + 2)); for (int rate : {8000, 12000, 16000, 24000, 48000}) { - std::array toneState{}; - Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0); - const int samples = 960 * rate / 48000; - std::array tone{}; - for (int frame = 0; frame < 4; ++frame) { - tone.fill(12345); - RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4 - 1); }); - Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4) == samples * 4); - Require(tone[0] == 12345 && tone[samples * 2 + 1] == 12345); - if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, 2, rate); + for (int channels : {1, 2}) { + std::array toneState{}; + Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, channels) == 0); + int error = OPUS_OK; + auto* reference = opus_decoder_create(rate, channels, &error); + Require(reference && error == OPUS_OK); + const int samples = 960 * rate / 48000; + std::array tone{}; + for (int frame = 0; frame < 4; ++frame) { + tone.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * channels * 2 - 1); }); + Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * channels * 2) == samples * channels * 2); + Require(tone[0] == 12345 && tone[samples * channels + 1] == 12345); + std::array expected{}; + Require(opus_decode(reference, CELT_TONE[frame], 60, expected.data(), samples, 0) == samples); + if (!std::equal(expected.begin(), expected.begin() + samples * channels, tone.begin() + 1)) { + std::fprintf(stderr, "CELT PCM differs from libopus at %d Hz, %d channels, frame %d\n", rate, channels, frame); + std::abort(); + } + if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, channels, rate); + } + opus_decoder_destroy(reference); + Require(sceOpusCeltDecDestroy(toneState.data()) == 0); } - Require(sceOpusCeltDecDestroy(toneState.data()) == 0); } Require(sceOpusCeltDecDestroy(state.data()) == 0); diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index e19fb78141..6446a6e8e3 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include extern "C" { int APS5_VABI sceOpusDecInitialize(std::uint32_t*); @@ -46,12 +49,56 @@ const std::uint8_t OPUS_NARROWBAND[] = { 10, 0, 0x08, 0x85, 0x89, 0x9D, 0x73, 0x0F, 0xC2, 0xB1, 0xE2, 0x48, }; +void CheckReference(int rate, int channels) { + std::uint32_t context = 0; + std::array state{}; + Require(sceOpusDecInitialize(&context) == 0); + Require(sceOpusDecCreateEx(&context, state.data(), rate, channels) == 0); + int error = OPUS_OK; + auto* reference = opus_decoder_create(rate, channels, &error); + Require(reference && error == OPUS_OK); + std::array longPacket{}; + auto* repacketizer = opus_repacketizer_create(); + Require(repacketizer != nullptr); + for (int i = 0; i < 6; ++i) Require(opus_repacketizer_cat(repacketizer, OPUS_STEREO + 2, OPUS_STEREO[0]) == OPUS_OK); + const int longSize = opus_repacketizer_out(repacketizer, longPacket.data() + 2, static_cast(longPacket.size() - 2)); + Require(longSize > 0); + opus_repacketizer_destroy(repacketizer); + longPacket[0] = static_cast(longSize); + longPacket[1] = static_cast(longSize >> 8); + for (auto packets : {std::span(OPUS_NARROWBAND), std::span(OPUS_STEREO), + std::span(OPUS_NARROWBAND), std::span(longPacket.data(), longSize + 2)}) { + for (std::size_t offset = 0; offset < packets.size();) { + const int bytes = packets[offset] | (packets[offset + 1] << 8); + const auto* packet = packets.data() + offset + 2; + std::array expected{}; + const int samples = opus_decode(reference, packet, bytes, expected.data(), 5760, 0); + Require(samples > 0); + const int count = samples * channels; + std::array actual{}; + actual.fill(12345); + Require(sceOpusDecDecode(state.data(), packet, bytes, actual.data() + 1, count * 2 + 1) == count * 2); + if (!std::equal(expected.begin(), expected.begin() + count, actual.begin() + 1)) { + std::fprintf(stderr, "Opus PCM differs from libopus at %d Hz, %d channels, packet offset %zu\n", rate, channels, offset); + std::abort(); + } + Require(actual.front() == 12345 && std::all_of(actual.begin() + count + 1, actual.end(), [](auto value) { return value == 12345; })); + offset += bytes + 2; + } + } + opus_decoder_destroy(reference); + Require(sceOpusDecDestroy(state.data()) == 0); + Require(sceOpusDecTerminate(&context) == 0); +} + } // namespace int main() { constexpr std::uint32_t errorBase = 0x81740000u; Require(sceOpusDecInitialize(nullptr) == static_cast(errorBase + 13)); Require(sceOpusDecDestroy(nullptr) == static_cast(errorBase)); + for (int rate : {8000, 12000, 16000, 24000, 48000}) + for (int channels : {1, 2}) CheckReference(rate, channels); std::uint32_t context = 0, otherContext = 0; Require(sceOpusDecInitialize(&context) == 0); std::array invalidState{}; diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 2b3827fa86..4ba1fc6999 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -8,4 +8,4 @@ ### Functional - [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. AJM error propagation, resource ownership and termination with live decoders are not implemented. Wrapper validation errors and their precedence are recovered. -- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; these inputs still throw. Packet loss concealment, asynchronous AJM execution, exact PCM conversion and resampler buffering remain outside the verified behavior of the FFmpeg implementation in [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp). +- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; packets whose TOC configuration is below 16 still throw. Decoding uses the bundled libopus decoder of [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) and shares its limits. Packet loss concealment and asynchronous AJM execution are not implemented. diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 6dbf108315..d98513d94b 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -6,6 +6,4 @@ ### Functional -- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout. -- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap -- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured +- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses bundled libopus 1.6.1 for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout. diff --git a/tools/package_release.py b/tools/package_release.py index 7c19c061cd..4279009b47 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -60,6 +60,8 @@ def package(platform, build, output, version): runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) files.extend(build / "core/libs/libs" / name for name in ("mimalloc.dll", "mimalloc-redirect.dll")) + if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}: + files.append(build / "core/libs/licenses/COPYING.opus") files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable for file in [*files, binary, *(source for source, _ in licenses)]: diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index e20eb74bd2..0ff7a04454 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -83,6 +83,25 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(archive.extractfile("libs/" + name).read(), name.encode()) self.assertEqual((self.output / "relinker-v1.exe").read_bytes(), b"Windows executable") + def test_opus_library_archives_include_the_bundled_license(self): + (self.source / "core/libs/prx/libSceOpusDec").mkdir() + (self.library.parent / "libSceOpusDec.prx").write_bytes(b"Opus library bytes") + license_file = self.build / "core/libs/licenses/COPYING.opus" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(b"Opus redistribution notice\n") + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/COPYING.opus"), license_file.read_bytes()) + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/COPYING.opus").read(), license_file.read_bytes()) + + def test_opus_release_without_its_license_is_rejected(self): + (self.source / "core/libs/prx/libSceOpusCeltDec").mkdir() + (self.library.parent / "libSceOpusCeltDec.prx").write_bytes(b"CELT library bytes") + with self.assertRaisesRegex(RuntimeError, "COPYING"): + package_release.package("linux", self.build, self.output, "v1") + self.assertFalse(self.output.exists()) + def test_windows_package_without_configured_compiler_preserves_existing_output(self): (self.build / "CMakeCache.txt").write_text("CMAKE_CXX_COMPILER:FILEPATH=compiler-NOTFOUND\n", encoding="utf-8") (self.build / "core/relinker/relinker.exe").write_bytes(b"Windows executable") From a6e9733c1783ebed7950f500370943f915979f13 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Thu, 22 Oct 2026 23:09:15 +0100 Subject: [PATCH 19/22] fix(opus): hide libopus symbols and package its notice as a license Build the bundled libopus with hidden visibility and an empty OPUS_EXPORT, so its symbols stay internal without relying on --exclude-libs (which Apple's linker lacks). Turn off OPUS_STACK_PROTECTOR and OPUS_FORTIFY_SOURCE so MinGW builds do not import libssp-0.dll, as in 27c0acabe2 from #9764. Ship the notice through the licenses list as libs/licenses/opus.txt, like vgmstream. Co-Authored-By: Claude Opus 5.5 --- cmake/Opus.cmake | 6 ++++-- tools/package_release.py | 4 ++-- tools/tests/test_package_release.py | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cmake/Opus.cmake b/cmake/Opus.cmake index 4f655afc78..d6bc3e3f78 100644 --- a/cmake/Opus.cmake +++ b/cmake/Opus.cmake @@ -6,9 +6,11 @@ function(add_bundled_opus) set(OPUS_BUILD_PROGRAMS OFF) set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF) set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF) + set(OPUS_STACK_PROTECTOR OFF) + set(OPUS_FORTIFY_SOURCE OFF) add_subdirectory(3rdparty/opus EXCLUDE_FROM_ALL) - configure_file(3rdparty/opus/COPYING ${CMAKE_BINARY_DIR}/core/libs/licenses/COPYING.opus COPYONLY) - set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON) + set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) + target_compile_definitions(opus PRIVATE OPUS_EXPORT=) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a) endif() diff --git a/tools/package_release.py b/tools/package_release.py index 4279009b47..4e0de7b406 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -56,12 +56,12 @@ def package(platform, build, output, version): licenses = [] if "libSceAjm.native.prx" in expected: licenses.append((ROOT / "3rdparty/vgmstream/COPYING", "libs/licenses/vgmstream.txt")) + if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}: + licenses.append((ROOT / "3rdparty/opus/COPYING", "libs/licenses/opus.txt")) if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) files.extend(build / "core/libs/libs" / name for name in ("mimalloc.dll", "mimalloc-redirect.dll")) - if expected & {"libSceOpusDec.prx", "libSceOpusCeltDec.prx"}: - files.append(build / "core/libs/licenses/COPYING.opus") files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable for file in [*files, binary, *(source for source, _ in licenses)]: diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 0ff7a04454..87c65025f7 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -86,14 +86,14 @@ class ReleasePackageTests(unittest.TestCase): def test_opus_library_archives_include_the_bundled_license(self): (self.source / "core/libs/prx/libSceOpusDec").mkdir() (self.library.parent / "libSceOpusDec.prx").write_bytes(b"Opus library bytes") - license_file = self.build / "core/libs/licenses/COPYING.opus" + license_file = self.source / "3rdparty/opus/COPYING" license_file.parent.mkdir(parents=True) license_file.write_bytes(b"Opus redistribution notice\n") package_release.package("linux", self.build, self.output, "v1") with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: - self.assertEqual(archive.read("libs/COPYING.opus"), license_file.read_bytes()) + self.assertEqual(archive.read("libs/licenses/opus.txt"), license_file.read_bytes()) with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: - self.assertEqual(archive.extractfile("libs/COPYING.opus").read(), license_file.read_bytes()) + self.assertEqual(archive.extractfile("libs/licenses/opus.txt").read(), license_file.read_bytes()) def test_opus_release_without_its_license_is_rejected(self): (self.source / "core/libs/prx/libSceOpusCeltDec").mkdir() From 5ad029659f6b0e01d79a8e47c15776aa9b5e01de Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:56:22 +0200 Subject: [PATCH 20/22] feat(libSceFreeType): implement guest list operations --- core/libs/prx/libSceFreeType/CMakeLists.txt | 14 + .../prx/libSceFreeType/include/GuestList.hpp | 33 +++ core/libs/prx/libSceFreeType/src/List.cpp | 101 +++++++ core/libs/tests/GuestFreeTypeList.cpp | 261 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 411 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/include/GuestList.hpp create mode 100644 core/libs/prx/libSceFreeType/src/List.cpp create mode 100644 core/libs/tests/GuestFreeTypeList.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 2292f26da8..6e479c5a37 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL + src/List.cpp Export.cpp src/GuestObjects.cpp src/Names.cpp @@ -24,6 +25,19 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() configure_windows_unwind(libSceFreeType) +if(BUILD_TESTING) + add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp) + target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_list_tests) + add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests) + set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFreeType/include/GuestList.hpp b/core/libs/prx/libSceFreeType/include/GuestList.hpp new file mode 100644 index 0000000000..67a8396b87 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestList.hpp @@ -0,0 +1,33 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTLIST_HPP +#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTLIST_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_LIST_H + +#include +#include + +namespace FreeType { + +using GuestListRec = FT_ListRec; +using GuestListNodeRec = FT_ListNodeRec; + +struct GuestMemoryRec { + void* user; + void* (APS5_VABI *alloc)(GuestMemoryRec*, std::int64_t); + void (APS5_VABI *free)(GuestMemoryRec*, void*); + void* (APS5_VABI *realloc)(GuestMemoryRec*, std::int64_t, std::int64_t, void*); +}; + +using GuestListIterator = int (APS5_VABI *)(GuestListNodeRec*, void*); +using GuestListDestructor = void (APS5_VABI *)(GuestMemoryRec*, void*, void*); + +static_assert(sizeof(GuestListNodeRec) == 24 && offsetof(GuestListNodeRec, prev) == 0 && offsetof(GuestListNodeRec, next) == 8 && offsetof(GuestListNodeRec, data) == 16); +static_assert(sizeof(GuestListRec) == 16 && offsetof(GuestListRec, head) == 0 && offsetof(GuestListRec, tail) == 8); +static_assert(sizeof(GuestMemoryRec) == 32 && offsetof(GuestMemoryRec, alloc) == 8 && offsetof(GuestMemoryRec, free) == 16 && offsetof(GuestMemoryRec, realloc) == 24); + +} + +#endif diff --git a/core/libs/prx/libSceFreeType/src/List.cpp b/core/libs/prx/libSceFreeType/src/List.cpp new file mode 100644 index 0000000000..ff3deb8272 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/List.cpp @@ -0,0 +1,101 @@ +#include "prx/libSceFreeType/include/GuestList.hpp" +#include "prx/libc/include/GuestArena.hpp" + +#include +#include + +using namespace FreeType; + +namespace { + +class ListWrite { +public: + ListWrite(GuestListRec* list, std::array nodes = {}) + : writes{GuestArena::HostWrite(list, sizeof(*list)), + GuestArena::HostWrite(nodes[0], nodes[0] ? sizeof(*nodes[0]) : 0), + GuestArena::HostWrite(nodes[1], nodes[1] ? sizeof(*nodes[1]) : 0), + GuestArena::HostWrite(nodes[2], nodes[2] ? sizeof(*nodes[2]) : 0), + GuestArena::HostWrite(nodes[3], nodes[3] ? sizeof(*nodes[3]) : 0)} { + for (const auto& write : writes) { + if (!write.Open()) throw std::runtime_error("FT_List: a destination is not writable"); + } + } + +private: + std::array writes; +}; + +struct Iteration { + GuestListIterator iterator; + void* user; +}; + +int Iterate(FT_ListNode node, void* user) { + auto& call = *static_cast(user); + return call.iterator(node, call.user); +} + +struct Finalization { + GuestMemoryRec* memory; + GuestListDestructor destroy; + void* user; +}; + +void Destroy(FT_Memory memory, void* data, void*) { + auto& call = *static_cast(memory->user); + call.destroy(call.memory, data, call.user); +} + +void FreeNode(FT_Memory memory, void* block) { + auto& call = *static_cast(memory->user); + call.memory->free(call.memory, block); +} + +} + +extern "C" { + +GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec* list, void* data) { + return FT_List_Find(list, data); +} + +void APS5_VABI FT_List_Add_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node) return; + const ListWrite write(list, {node, list->tail}); + FT_List_Add(list, node); +} + +void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node) return; + const ListWrite write(list, {node, list->head}); + FT_List_Insert(list, node); +} + +void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node) return; + const ListWrite write(list, {node->prev, node->next}); + FT_List_Remove(list, node); +} + +void APS5_VABI FT_List_Up_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { + if (!list || !node || !node->prev) return; + const ListWrite write(list, {node, node->prev, node->next, list->head}); + FT_List_Up(list, node); +} + +int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec* list, GuestListIterator iterator, void* user) { + Iteration call{iterator, user}; + return FT_List_Iterate(list, iterator ? Iterate : nullptr, &call); +} + +void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec* list, GuestListDestructor destroy, GuestMemoryRec* memory, void* user) { + if (!list || !memory) return; + const ListWrite write(list); + Finalization call{memory, destroy, user}; + FT_MemoryRec_ native{}; + native.user = &call; + native.free = FreeNode; + FT_List_Finalize(list, destroy ? Destroy : nullptr, &native, user); +} + +} diff --git a/core/libs/tests/GuestFreeTypeList.cpp b/core/libs/tests/GuestFreeTypeList.cpp new file mode 100644 index 0000000000..ba2a805e48 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeList.cpp @@ -0,0 +1,261 @@ +#include "prx/libSceFreeType/include/GuestList.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec*, void*); +void APS5_VABI FT_List_Add_nid_postfix(GuestListRec*, GuestListNodeRec*); +void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec*, GuestListNodeRec*); +void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec*, GuestListNodeRec*); +void APS5_VABI FT_List_Up_nid_postfix(GuestListRec*, GuestListNodeRec*); +int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec*, GuestListIterator, void*); +void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec*, GuestListDestructor, GuestMemoryRec*, void*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType list check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +constexpr std::size_t Count = 16; +using Nodes = std::array; + +static int Index(FT_ListNode node, const Nodes& nodes) { + if (!node) return -1; + for (std::size_t i = 0; i < nodes.size(); ++i) if (node == &nodes[i]) return static_cast(i); + Require(false); + return -2; +} + +struct Lists { + FT_ListRec guest{}; + FT_ListRec native{}; + Nodes actual{}; + Nodes expected{}; + std::array values{11, 22, 33, 44}; + + Lists() { + for (std::size_t i = 0; i < Count; ++i) actual[i].data = expected[i].data = i % 5 == 0 ? nullptr : &values[i % values.size()]; + } + + void Match() const { + Require(Index(guest.head, actual) == Index(native.head, expected)); + Require(Index(guest.tail, actual) == Index(native.tail, expected)); + for (std::size_t i = 0; i < Count; ++i) { + Require(Index(actual[i].prev, actual) == Index(expected[i].prev, expected)); + Require(Index(actual[i].next, actual) == Index(expected[i].next, expected)); + Require(actual[i].data == expected[i].data); + } + } + + void Fill() { + for (std::size_t i = 0; i < Count; ++i) { + FT_List_Add_nid_postfix(&guest, &actual[i]); + FT_List_Add(&native, &expected[i]); + } + Match(); + } +}; + +static void Mutations() { + Lists lists; + std::array active{}; + std::array operations{}; + std::mt19937 random(0x7157); + for (unsigned step = 0; step < 2048; ++step) { + const auto i = random() % Count; + const bool choice = (random() & 1u) != 0; + if (active[i]) { + if (choice) { + FT_List_Up_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Up(&lists.native, &lists.expected[i]); + ++operations[3]; + } else { + FT_List_Remove_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Remove(&lists.native, &lists.expected[i]); + active[i] = false; + ++operations[2]; + } + } else { + if (choice) { + FT_List_Insert_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Insert(&lists.native, &lists.expected[i]); + ++operations[1]; + } else { + FT_List_Add_nid_postfix(&lists.guest, &lists.actual[i]); + FT_List_Add(&lists.native, &lists.expected[i]); + ++operations[0]; + } + active[i] = true; + } + lists.Match(); + for (auto& value : lists.values) { + Require(Index(FT_List_Find_nid_postfix(&lists.guest, &value), lists.actual) == Index(FT_List_Find(&lists.native, &value), lists.expected)); + } + Require(Index(FT_List_Find_nid_postfix(&lists.guest, nullptr), lists.actual) == Index(FT_List_Find(&lists.native, nullptr), lists.expected)); + int absent = 123; + Require(FT_List_Find_nid_postfix(&lists.guest, &absent) == nullptr); + } + for (const auto count : operations) Require(count != 0); + for (auto operation : {FT_List_Add_nid_postfix, FT_List_Insert_nid_postfix, FT_List_Remove_nid_postfix, FT_List_Up_nid_postfix}) { + operation(nullptr, &lists.actual[0]); + operation(&lists.guest, nullptr); + operation(nullptr, nullptr); + lists.Match(); + } + Require(FT_List_Find_nid_postfix(nullptr, nullptr) == nullptr); + std::printf("list mutations: 2048 states compared with native FreeType\n"); +} + +struct Walk { + FT_List list; + Nodes* nodes; + bool guest; + bool remove; + std::size_t stop; + int error; + std::vector visits; +}; + +static int Visit(FT_ListNode node, void* user, bool guest) { + auto& state = *static_cast(user); + Require(state.guest == guest); + state.visits.push_back(Index(node, *state.nodes)); + if (state.remove) { + if (guest) FT_List_Remove_nid_postfix(state.list, node); + else FT_List_Remove(state.list, node); + node->prev = nullptr; + node->next = nullptr; + } + return state.visits.size() == state.stop ? state.error : 0; +} + +static int NativeIterator(FT_ListNode node, void* user) { return Visit(node, user, false); } +static int APS5_VABI GuestIterator(GuestListNodeRec* node, void* user) { return Visit(node, user, true); } + +static void IterationCases() { + for (const bool remove : {false, true}) { + for (const std::size_t stop : {0u, 1u, 3u, 16u}) { + Lists lists; + lists.Fill(); + const int error = stop == 3 ? -29 : 73; + Walk guest{&lists.guest, &lists.actual, true, remove, stop, error, {}}; + Walk native{&lists.native, &lists.expected, false, remove, stop, error, {}}; + const int result = FT_List_Iterate_nid_postfix(&lists.guest, GuestIterator, &guest); + Require(result == FT_List_Iterate(&lists.native, NativeIterator, &native)); + Require(result == (stop == 0 ? 0 : error)); + Require(guest.visits == native.visits && guest.visits.size() == (stop == 0 ? Count : stop)); + lists.Match(); + } + } + Lists empty; + Walk guest{&empty.guest, &empty.actual, true, false, 0, 0, {}}; + Walk native{&empty.native, &empty.expected, false, false, 0, 0, {}}; + Require(FT_List_Iterate_nid_postfix(&empty.guest, GuestIterator, &guest) == 0 && guest.visits.empty()); + Require(FT_List_Iterate_nid_postfix(nullptr, GuestIterator, &guest) == FT_List_Iterate(nullptr, NativeIterator, &native)); + Require(FT_List_Iterate_nid_postfix(&empty.guest, nullptr, &guest) == FT_List_Iterate(&empty.native, nullptr, &native)); + Require(FT_List_Iterate_nid_postfix(nullptr, nullptr, nullptr) == FT_List_Iterate(nullptr, nullptr, nullptr)); + std::printf("list iteration: 8 callback/order/error/removal cases compared\n"); +} + +struct Event { + int kind; + int id; + bool cleared; + bool operator==(const Event&) const = default; +}; + +struct Disposal { + FT_List list; + FT_Memory native; + GuestMemoryRec* guest; + void* user; + std::map live; + std::vector events; +}; + +static void Release(Disposal& state, void* block) { + const auto found = state.live.find(block); + Require(found != state.live.end()); + state.events.push_back({2, found->second, state.list->head == nullptr || state.list->tail == nullptr}); + state.live.erase(found); + delete static_cast(block); +} + +static void NativeFree(FT_Memory memory, void* block) { + auto& state = *static_cast(memory->user); + Require(state.native == memory); + Release(state, block); +} + +static void APS5_VABI GuestFree(GuestMemoryRec* memory, void* block) { + auto& state = *static_cast(memory->user); + Require(state.guest == memory); + Release(state, block); +} + +static void NativeDestroy(FT_Memory memory, void* data, void* user) { + auto& state = *static_cast(memory->user); + Require(state.native == memory && state.user == user); + state.events.push_back({1, data ? *static_cast(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); +} + +static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* user) { + auto& state = *static_cast(memory->user); + Require(state.guest == memory && state.user == user); + state.events.push_back({1, data ? *static_cast(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); +} + +static void FinalizationCases() { + for (const bool destroy : {false, true}) { + for (const unsigned count : {0u, 1u, 8u}) { + FT_ListRec guestList{}, nativeList{}; + std::array ids{0, 1, 2, 3, 4, 5, 6, 7}; + int token = 9; + Disposal guest{&guestList, nullptr, nullptr, &token, {}, {}}; + Disposal native{&nativeList, nullptr, nullptr, &token, {}, {}}; + GuestMemoryRec guestMemory{&guest, nullptr, GuestFree, nullptr}; + FT_MemoryRec_ nativeMemory{&native, nullptr, NativeFree, nullptr}; + guest.guest = &guestMemory; + native.native = &nativeMemory; + for (unsigned i = 0; i < count; ++i) { + auto* a = new FT_ListNodeRec{}; + auto* b = new FT_ListNodeRec{}; + a->data = b->data = i == 0 ? nullptr : &ids[i]; + guest.live.emplace(a, i); + native.live.emplace(b, i); + FT_List_Add_nid_postfix(&guestList, a); + FT_List_Add(&nativeList, b); + } + const auto savedHead = guestList.head; + FT_List_Finalize_nid_postfix(&guestList, GuestDestroy, nullptr, &token); + Require(guestList.head == savedHead && guest.events.empty()); + FT_List_Finalize_nid_postfix(nullptr, GuestDestroy, &guestMemory, &token); + Require(guest.events.empty()); + FT_List_Finalize_nid_postfix(&guestList, destroy ? GuestDestroy : nullptr, &guestMemory, &token); + FT_List_Finalize(&nativeList, destroy ? NativeDestroy : nullptr, &nativeMemory, &token); + Require(guestList.head == nullptr && guestList.tail == nullptr && nativeList.head == nullptr && nativeList.tail == nullptr); + Require(guest.live.empty() && native.live.empty()); + Require(guest.events == native.events && guest.events.size() == count * (destroy ? 2u : 1u)); + for (const auto& event : guest.events) Require(!event.cleared); + } + } + std::printf("list finalization: 6 allocator/destructor/order cases compared\n"); +} + +int main() { + Mutations(); + IterationCases(); + FinalizationCases(); +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index b2a706294a..24d6d994ff 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -16,6 +16,8 @@ - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. +- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated. + - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read From 217df9a44cde9640fca4c6b6d622178e534fee7c Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:59:47 +0200 Subject: [PATCH 21/22] build(libSceFreeType): keep list registration independent of pending features --- core/libs/prx/libSceFreeType/CMakeLists.txt | 28 ++++++++++----------- docs/dev/debt/libSceFreeType.md | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 6e479c5a37..060d3f7830 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL - src/List.cpp Export.cpp src/GuestObjects.cpp src/Names.cpp @@ -20,24 +19,12 @@ if(BUILD_TESTING) endif() endif() +target_sources(libSceFreeType PRIVATE src/List.cpp) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceFreeType PRIVATE LINKER:--exclude-libs,ALL) endif() configure_windows_unwind(libSceFreeType) -if(BUILD_TESTING) - add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp) - target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc) - configure_windows_unwind(guest_freetype_list_tests) - add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests) - set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20) - if(WIN32) - set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() -endif() - if(BUILD_TESTING) add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -70,3 +57,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_freetype_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeList.cpp) + target_include_directories(guest_freetype_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_list_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_list_tests) + add_test(NAME guest_freetype_list COMMAND guest_freetype_list_tests) + set_tests_properties(guest_freetype_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 24d6d994ff..62890d99b5 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -16,10 +16,10 @@ - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. -- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated. - - [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. +- [FT_List_Add, FT_List_Insert, FT_List_Find, FT_List_Remove, FT_List_Up, FT_List_Iterate and FT_List_Finalize](../../../core/libs/prx/libSceFreeType/src/List.cpp) - console signatures, callback ordering and error compatibility are unverified. Names and NIDs follow the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the pointer-only 16-byte list, 24-byte node and operations follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftutil.c). The 32-byte allocator record uses LP64 guest callbacks, and finalization calls its guest `free` callback for each node. Native list-membership and callback preconditions still apply; malformed lists and callbacks that invalidate a later node are not validated. + - [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read - [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. From 616bd98623c933495577525b6a6317bc0dd58088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 09:30:05 +0200 Subject: [PATCH 22/22] fix(main): raise conventions shard timeout to 120s Failing run 39915816778 (windows conventions_function_bodies_shard_1 Timeout 30.06s; sibling shards took 27s on the same runner). No culprit merge: the 30s limit sat at the edge of normal runtime. Linux failure in that run was a runner-side submodule shallow.lock (infra). --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fba7c7a18b..25d411aa58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,7 +169,7 @@ if(BUILD_TESTING) endif() add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12) - set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30) + set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 120) endforeach() add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py)